diff --git a/src/components/Insights/InsightJiraTicket/InsightJiraTicket.stories.tsx b/src/components/Insights/InsightJiraTicket/InsightJiraTicket.stories.tsx index f5f8388cd..3ba20811a 100644 --- a/src/components/Insights/InsightJiraTicket/InsightJiraTicket.stories.tsx +++ b/src/components/Insights/InsightJiraTicket/InsightJiraTicket.stories.tsx @@ -69,7 +69,9 @@ export const Linked: Story = { args: { summary: "Summary text", description: { content: "Multiline\ndescription text", isLoading: false }, - attachment: { url: "https://www.example.com", fileName: "attachment.ext" }, + attachments: [ + { url: "https://www.example.com", fileName: "attachment.ext" } + ], insight: { ...insight, ticketLink: "https://digma.ai/ticket/1" } } }; @@ -78,7 +80,32 @@ export const Unlinked: Story = { args: { summary: "", description: { content: "Multiline\ndescription text", isLoading: false }, - attachment: { url: "https://www.example.com", fileName: "attachment.ext" }, - insight + attachments: [ + { url: "https://www.example.com", fileName: "attachment.ext" } + ], + insight: { ...insight, ticketLink: "https://digma.ai/ticket/1" } + } +}; + +export const SingleAttachment: Story = { + args: { + summary: "", + description: { content: "Description text", isLoading: false }, + attachments: [ + { url: "https://www.example.com", fileName: "attachment.ext" } + ], + insight: { ...insight, ticketLink: "https://digma.ai/ticket/1" } + } +}; + +export const MultiAttachments: Story = { + args: { + summary: "Summary text", + description: { content: "Description text", isLoading: false }, + attachments: [ + { url: "https://www.example.com", fileName: "attachment_1.ext" }, + { url: "https://www.example.com", fileName: "attachment_2.ext" } + ], + insight: { ...insight, ticketLink: "https://digma.ai/ticket/1" } } }; diff --git a/src/components/Insights/InsightJiraTicket/index.tsx b/src/components/Insights/InsightJiraTicket/index.tsx index b5611b306..3bf8a9753 100644 --- a/src/components/Insights/InsightJiraTicket/index.tsx +++ b/src/components/Insights/InsightJiraTicket/index.tsx @@ -11,6 +11,7 @@ import { InsightsGetDataListQuery, LinkTicketResponse } from "./types"; +import { Attachment } from "../../common/JiraTicket/types"; export const InsightJiraTicket = (props: InsightJiraTicketProps) => { const [errorMessage, setErrorMessage] = useState(); @@ -95,7 +96,7 @@ export const InsightJiraTicket = (props: InsightJiraTicketProps) => { void; diff --git a/src/components/Insights/tickets/EndpointNPlusOneInsightTicket/index.tsx b/src/components/Insights/tickets/EndpointNPlusOneInsightTicket/index.tsx index 83ecd9fdf..423963ebe 100644 --- a/src/components/Insights/tickets/EndpointNPlusOneInsightTicket/index.tsx +++ b/src/components/Insights/tickets/EndpointNPlusOneInsightTicket/index.tsx @@ -73,12 +73,16 @@ export const EndpointNPlusOneInsightTicket = ( ); const traceId = span?.traceId; - const attachment = traceId - ? { - url: `${config.jaegerURL}/api/traces/${traceId}?prettyPrint=true`, - fileName: `trace-${traceId}.json` - } - : undefined; + const attachments = [ + ...(traceId + ? [ + { + url: `${config.jaegerURL}/api/traces/${traceId}?prettyPrint=true`, + fileName: `trace-${traceId}.json` + } + ] + : []) + ]; return ( diff --git a/src/components/Insights/tickets/QueryOptimizationInsightTicket/index.tsx b/src/components/Insights/tickets/QueryOptimizationInsightTicket/index.tsx index 68865d869..29d6bf373 100644 --- a/src/components/Insights/tickets/QueryOptimizationInsightTicket/index.tsx +++ b/src/components/Insights/tickets/QueryOptimizationInsightTicket/index.tsx @@ -80,12 +80,16 @@ export const QueryOptimizationInsightTicket = ( ); const traceId = props.data.insight.traceId; - const attachment = traceId - ? { - url: `${config.jaegerURL}/api/traces/${traceId}?prettyPrint=true`, - fileName: `trace-${traceId}.json` - } - : undefined; + const attachments = [ + ...(traceId + ? [ + { + url: `${config.jaegerURL}/api/traces/${traceId}?prettyPrint=true`, + fileName: `trace-${traceId}.json` + } + ] + : []) + ]; return ( diff --git a/src/components/Insights/tickets/ScalingIssueInsightTicket/index.tsx b/src/components/Insights/tickets/ScalingIssueInsightTicket/index.tsx index 95de4010f..08c394cad 100644 --- a/src/components/Insights/tickets/ScalingIssueInsightTicket/index.tsx +++ b/src/components/Insights/tickets/ScalingIssueInsightTicket/index.tsx @@ -79,10 +79,11 @@ export const ScalingIssueInsightTicket = ( ?.map((ep) => ep.sampleTraceId) ?.find((t) => t); const attachmentTrace = getTraceAttachment(config, traceId); - - // Add it to the attachment(s) after we'll support more than one and - // know how to make https calls while ignoring ssl cert verification const attachmentHistogram = getHistogramAttachment(config, insight); + const attachments = [ + ...(attachmentTrace ? [attachmentTrace] : []), + ...(attachmentHistogram ? [attachmentHistogram] : []) + ]; return ( diff --git a/src/components/Insights/tickets/ScalingIssueInsightTicketByRootCause/index.tsx b/src/components/Insights/tickets/ScalingIssueInsightTicketByRootCause/index.tsx index b9e0a6ab5..88c01d094 100644 --- a/src/components/Insights/tickets/ScalingIssueInsightTicketByRootCause/index.tsx +++ b/src/components/Insights/tickets/ScalingIssueInsightTicketByRootCause/index.tsx @@ -84,10 +84,11 @@ export const ScalingIssueInsightTicketByRootCause = ( const summary = getScalingIssueSummary(spanInsight); const attachmentTrace = getTraceAttachment(config, spanInfo?.sampleTraceId); - - // Add it to the attachment(s) after we'll support more than one and - // know how to make https calls while ignoring ssl cert verification const attachmentHistogram = getHistogramAttachment(config, spanInsight); + const attachments = [ + ...(attachmentTrace ? [attachmentTrace] : []), + ...(attachmentHistogram ? [attachmentHistogram] : []) + ]; return ( { const { @@ -51,12 +52,16 @@ export const TestTicket = (props: TestTicketProps) => { ); - const attachment = traceId - ? { - url: `${config.jaegerURL}/api/traces/${traceId}?prettyPrint=true`, - fileName: `trace-${traceId}.json` - } - : undefined; + const attachments = [ + ...(traceId + ? [ + { + url: `${config.jaegerURL}/api/traces/${traceId}?prettyPrint=true`, + fileName: `trace-${traceId}.json` + } + ] + : []) + ]; return ( { description={{ content: renderDescription() }} - attachment={attachment} + attachments={attachments} onClose={props.onClose} tracking={{ prefix: "tests" }} /> diff --git a/src/components/common/JiraTicket/AttachmentTag/index.tsx b/src/components/common/JiraTicket/AttachmentTag/index.tsx index 6d55bd2ba..fdf6e1266 100644 --- a/src/components/common/JiraTicket/AttachmentTag/index.tsx +++ b/src/components/common/JiraTicket/AttachmentTag/index.tsx @@ -5,7 +5,7 @@ import { AttachmentTagProps } from "./types"; export const AttachmentTag = (props: AttachmentTagProps) => ( - + {props.text} diff --git a/src/components/common/JiraTicket/AttachmentTag/styles.ts b/src/components/common/JiraTicket/AttachmentTag/styles.ts index 2e072b7bb..7b9f601fd 100644 --- a/src/components/common/JiraTicket/AttachmentTag/styles.ts +++ b/src/components/common/JiraTicket/AttachmentTag/styles.ts @@ -2,11 +2,8 @@ import styled from "styled-components"; export const Container = styled.div` display: flex; - padding: 4px 6px 4px 4px; + padding: 2px 0; gap: 8px; - border-radius: 4px; - border: 1px solid ${({ theme }) => theme.colors.attachmentTag.border}; - background: ${({ theme }) => theme.colors.attachmentTag.background}; color: ${({ theme }) => theme.colors.attachmentTag.text}; align-items: center; max-width: fit-content; diff --git a/src/components/common/JiraTicket/Field/index.tsx b/src/components/common/JiraTicket/Field/index.tsx index 891419311..fce3169db 100644 --- a/src/components/common/JiraTicket/Field/index.tsx +++ b/src/components/common/JiraTicket/Field/index.tsx @@ -1,7 +1,6 @@ import { useCallback, useRef } from "react"; import useDimensions from "react-cool-dimensions"; import useScrollbarSize from "react-scrollbar-size"; -import { isString } from "../../../../typeGuards/isString"; import * as s from "./styles"; import { ButtonPosition, FieldProps } from "./types"; @@ -28,22 +27,16 @@ export const Field = (props: FieldProps) => { props.multiline === true ? "top" : "center"; return ( - - {props.label} - - - {props.content} - - {props.button} - - - - {isString(props.errorMessage) && ( - {props.errorMessage} - )} - + + + {props.children} + + {props.button} + + + ); }; diff --git a/src/components/common/JiraTicket/Field/styles.ts b/src/components/common/JiraTicket/Field/styles.ts index 32e16842a..737aa479c 100644 --- a/src/components/common/JiraTicket/Field/styles.ts +++ b/src/components/common/JiraTicket/Field/styles.ts @@ -1,17 +1,5 @@ import styled from "styled-components"; -import { redScale } from "../../../common/App/v2colors"; -import { ButtonContainerProps, ContainerProps, ContentProps } from "./types"; - -export const Container = styled.div` - display: flex; - flex-direction: column; - gap: 6px; - ${({ $selectable }) => ($selectable ? "" : "user-select: none;")} -`; - -export const Label = styled.label` - color: ${({ theme }) => theme.colors.jiraTicket.text.secondary}; -`; +import { ButtonContainerProps, ContentProps } from "./types"; export const ContentContainer = styled.div` display: flex; @@ -48,19 +36,3 @@ export const ButtonContainer = styled.div` } }} `; - -export const ErrorMessage = styled.span` - display: flex; - font-size: 13px; - align-items: center; - white-space: pre-line; - color: ${({ theme }) => { - switch (theme.mode) { - case "light": - return redScale[500]; - case "dark": - case "dark-jetbrains": - return redScale[300]; - } - }}; -`; diff --git a/src/components/common/JiraTicket/Field/types.ts b/src/components/common/JiraTicket/Field/types.ts index dd14d5a7a..a49dfa2fc 100644 --- a/src/components/common/JiraTicket/Field/types.ts +++ b/src/components/common/JiraTicket/Field/types.ts @@ -9,12 +9,9 @@ export interface FieldThemeColors { export type ButtonPosition = "top" | "center"; export interface FieldProps { - content: ReactNode; - label: string; + children: ReactNode; button: ReactNode; multiline?: boolean; - errorMessage?: string; - selectable?: boolean; } export interface ButtonContainerProps { @@ -25,7 +22,3 @@ export interface ButtonContainerProps { export interface ContentProps { $multiline?: boolean; } - -export interface ContainerProps { - $selectable?: boolean; -} diff --git a/src/components/common/JiraTicket/IconButton/index.tsx b/src/components/common/JiraTicket/IconButton/index.tsx index 1124f2f23..09485a69a 100644 --- a/src/components/common/JiraTicket/IconButton/index.tsx +++ b/src/components/common/JiraTicket/IconButton/index.tsx @@ -5,7 +5,7 @@ import { IconButtonProps } from "./types"; export const IconButton = (props: IconButtonProps) => ( - + ); diff --git a/src/components/common/JiraTicket/IconButton/types.ts b/src/components/common/JiraTicket/IconButton/types.ts index b3a6a8095..c48c8074c 100644 --- a/src/components/common/JiraTicket/IconButton/types.ts +++ b/src/components/common/JiraTicket/IconButton/types.ts @@ -6,4 +6,5 @@ export interface IconButtonProps { onClick: () => void; title: string; disabled?: boolean; + size?: number; } diff --git a/src/components/common/JiraTicket/JiraTicket.stories.tsx b/src/components/common/JiraTicket/JiraTicket.stories.tsx index b35dc8b9e..0746a57f4 100644 --- a/src/components/common/JiraTicket/JiraTicket.stories.tsx +++ b/src/components/common/JiraTicket/JiraTicket.stories.tsx @@ -19,6 +19,8 @@ export const Default: Story = { args: { summary: "Summary text", description: { content: "Multiline\ndescription text", isLoading: false }, - attachment: { url: "https://www.example.com", fileName: "attachment.ext" } + attachments: [ + { url: "https://www.example.com", fileName: "attachment.ext" } + ] } }; diff --git a/src/components/common/JiraTicket/Section/index.tsx b/src/components/common/JiraTicket/Section/index.tsx new file mode 100644 index 000000000..765def69b --- /dev/null +++ b/src/components/common/JiraTicket/Section/index.tsx @@ -0,0 +1,15 @@ +import { isString } from "../../../../typeGuards/isString"; +import * as s from "./styles"; +import { SectionProps } from "./types"; + +export const Section = (props: SectionProps) => { + return ( + + {props.title} + {props.children} + {isString(props.errorMessage) && ( + {props.errorMessage} + )} + + ); +}; diff --git a/src/components/common/JiraTicket/Section/styles.ts b/src/components/common/JiraTicket/Section/styles.ts new file mode 100644 index 000000000..60aca3953 --- /dev/null +++ b/src/components/common/JiraTicket/Section/styles.ts @@ -0,0 +1,30 @@ +import styled from "styled-components"; +import { redScale } from "../../../common/App/v2colors"; +import { ContainerProps } from "./types"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 6px; + ${({ $selectable }) => ($selectable ? "" : "user-select: none;")} +`; + +export const Label = styled.label` + color: ${({ theme }) => theme.colors.jiraTicket.text.secondary}; +`; + +export const ErrorMessage = styled.span` + display: flex; + font-size: 13px; + align-items: center; + white-space: pre-line; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return redScale[500]; + case "dark": + case "dark-jetbrains": + return redScale[300]; + } + }}; +`; diff --git a/src/components/common/JiraTicket/Section/types.ts b/src/components/common/JiraTicket/Section/types.ts new file mode 100644 index 000000000..180c40414 --- /dev/null +++ b/src/components/common/JiraTicket/Section/types.ts @@ -0,0 +1,12 @@ +import { ReactNode } from "react"; + +export interface SectionProps { + children: ReactNode; + title: string; + errorMessage?: string; + selectable?: boolean; +} + +export interface ContainerProps { + $selectable?: boolean; +} diff --git a/src/components/common/JiraTicket/index.tsx b/src/components/common/JiraTicket/index.tsx index 370d15733..624c2030f 100644 --- a/src/components/common/JiraTicket/index.tsx +++ b/src/components/common/JiraTicket/index.tsx @@ -17,6 +17,7 @@ import { PaperclipIcon } from "../../common/icons/12px/PaperclipIcon"; import { JiraLogoIcon } from "../../common/icons/16px/JiraLogoIcon"; import { AttachmentTag } from "./AttachmentTag"; import { Field } from "./Field"; +import { Section } from "./Section"; import { IconButton } from "./IconButton"; import { TicketLinkButton } from "./TicketLinkButton"; import * as s from "./styles"; @@ -79,23 +80,25 @@ export const JiraTicket = (props: JiraTicketProps) => { } }; - const handleDownloadButtonClick = () => { + const handleDownloadButtonClick = (attachment: { + url: string; + fileName: string; + }) => { sendTrackingEvent( prefixedTrackingEvents.JIRA_TICKET_ATTACHMENT_DOWNLOAD_BUTTON_CLICKED, { ...(props.tracking?.additionalInfo || {}) } ); - if (props.attachment) { - downloadFile(props.attachment.url, props.attachment.fileName).catch( - (e) => { - const errorMessageString = - e instanceof Error ? `Error: ${e.message}` : ""; - setDownloadErrorMessage( - `Failed to download file.\n${errorMessageString}` - ); - } - ); - } + downloadFile(attachment.url, attachment.fileName).catch( + // tmp + (e) => { + const errorMessageString = + e instanceof Error ? `Error: ${e.message}` : ""; + setDownloadErrorMessage( + `Failed to download file.\n${errorMessageString}` + ); + } + ); }; const errorMessage = props.description.isLoading @@ -116,25 +119,38 @@ export const JiraTicket = (props: JiraTicketProps) => { - copyToClipboard("summary", props.summary)} - /> - } - selectable={false} - /> - + copyToClipboard("summary", props.summary)} + /> + } + > + {props.summary} + + +
+ + copyToClipboard("description", descriptionContentRef.current) + } + /> + } + >
{props.description.isLoading ? ( @@ -144,38 +160,35 @@ export const JiraTicket = (props: JiraTicketProps) => { props.description.content )}
- } - errorMessage={errorMessage} - button={ - - copyToClipboard("description", descriptionContentRef.current) - } - /> - } - /> - {props.attachment && ( - +
+ {props.attachments.length > 0 && ( +
- } - button={ - - } + title={"Attachments"} errorMessage={downloadErrorMessage} - /> + > + {props.attachments.map((attachment, i) => { + return ( + handleDownloadButtonClick(attachment)} + /> + } + > + + + ); + })} +
)} {props.showLinkButton && ( void; tracking?: { prefix?: string; @@ -32,3 +32,8 @@ export interface JiraTicketProps { unlinkTicket?: () => void; linkTicket?: (link: string) => void; } + +export interface Attachment { + url: string; + fileName: string; +}