From 896c9056aa18849ab5fe17d56b50ce6eed38cfce Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Mon, 8 Jan 2024 16:57:11 +0100 Subject: [PATCH 01/13] Add Tests app --- .storybook/preview-body.html | 2 ++ package.json | 2 ++ src/components/Tests/Tests.stories.tsx | 20 ++++++++++++++++ src/components/Tests/index.tsx | 32 ++++++++++++++++++++++++++ src/components/Tests/styles.ts | 6 +++++ src/components/common/App/getTheme.ts | 6 ++++- src/containers/Tests/index.tsx | 27 ++++++++++++++++++++++ src/containers/Tests/styles.ts | 7 ++++++ src/globals.d.ts | 1 + src/styled.d.ts | 3 +++ webpackEntries.ts | 3 +++ 11 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/components/Tests/Tests.stories.tsx create mode 100644 src/components/Tests/index.tsx create mode 100644 src/components/Tests/styles.ts create mode 100644 src/containers/Tests/index.tsx create mode 100644 src/containers/Tests/styles.ts diff --git a/.storybook/preview-body.html b/.storybook/preview-body.html index 02ce46f25..f17ab4804 100644 --- a/.storybook/preview-body.html +++ b/.storybook/preview-body.html @@ -34,6 +34,8 @@ "https://github.com/digma-ai/digma-vscode-plugin#%EF%B8%8F-extension-settings"; window.recentActivityIsEnvironmentManagementEnabled = true; + window.testsRefreshInterval; + window.wizardFirstLaunch = true; window.wizardSkipInstallationStep; diff --git a/package.json b/package.json index 223782271..9390a4eaf 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "build:installation-wizard:dev": "webpack --config webpack.dev.ts --env app=installationWizard", "build:notifications:dev": "webpack --config webpack.dev.ts --env app=notifications", "build:recentActivity:dev": "webpack --config webpack.dev.ts --env app=recentActivity", + "build:tests:dev": "webpack --config webpack.dev.ts --env app=tests", "build:troubleshooting:dev": "webpack --config webpack.dev.ts --env app=troubleshooting", "build:dev": "webpack --config webpack.dev.ts", "build:dev:web": "webpack --config webpack.dev.ts --env platform=Web", @@ -28,6 +29,7 @@ "build:installation-wizard:prod": "webpack --config webpack.prod.ts --env app=installationWizard", "build:notifications:prod": "webpack --config webpack.prod.ts --env app=notifications", "build:recentActivity:prod": "webpack --config webpack.prod.ts --env app=recentActivity", + "build:tests:prod": "webpack --config webpack.prod.ts --env app=tests", "build:troubleshooting:prod": "webpack --config webpack.prod.ts --env app=troubleshooting", "build:prod": "webpack --config webpack.prod.ts", "build:prod:web": "webpack --config webpack.prod.ts --env platform=Web", diff --git a/src/components/Tests/Tests.stories.tsx b/src/components/Tests/Tests.stories.tsx new file mode 100644 index 000000000..9ad7e31b7 --- /dev/null +++ b/src/components/Tests/Tests.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { Tests } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Tests/Tests", + component: Tests, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Default: Story = {}; diff --git a/src/components/Tests/index.tsx b/src/components/Tests/index.tsx new file mode 100644 index 000000000..b2e9c4d36 --- /dev/null +++ b/src/components/Tests/index.tsx @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import { addPrefix } from "../../utils/addPrefix"; +import { sendTrackingEvent } from "../../utils/sendTrackingEvent"; +import * as s from "./styles"; + +const ACTION_PREFIX = "TESTS"; + +const actions = addPrefix(ACTION_PREFIX, { + INITIALIZE: "INITIALIZE" +}); + +const TRACKING_PREFIX = "tests"; + +export const trackingEvents = addPrefix( + TRACKING_PREFIX, + { + PAGE_LOADED: "page loaded" + }, + " " +); + +export const Tests = () => { + useEffect(() => { + window.sendMessageToDigma({ + action: actions.INITIALIZE + }); + + sendTrackingEvent(trackingEvents.PAGE_LOADED); + }, []); + + return Tests; +}; diff --git a/src/components/Tests/styles.ts b/src/components/Tests/styles.ts new file mode 100644 index 000000000..5b0057276 --- /dev/null +++ b/src/components/Tests/styles.ts @@ -0,0 +1,6 @@ +import styled from "styled-components"; + +export const Container = styled.div` + background: ${({ theme }) => theme.colors.panel.background}; + min-height: 100vh; +`; diff --git a/src/components/common/App/getTheme.ts b/src/components/common/App/getTheme.ts index 07a990b54..24634e940 100644 --- a/src/components/common/App/getTheme.ts +++ b/src/components/common/App/getTheme.ts @@ -234,6 +234,9 @@ const darkThemeColors: ThemeColors = { border: grayScale[700], icon: grayScale[200], text: grayScale[100] + }, + panel: { + background: grayScale[1000] } }; @@ -415,7 +418,8 @@ const lightThemeColors: ThemeColors = { border: grayScale[300], icon: grayScale[800], text: grayScale[800] - } + }, + panel: { background: grayScale[150] } }; const getColors = (mode: Mode): ThemeColors => { diff --git a/src/containers/Tests/index.tsx b/src/containers/Tests/index.tsx new file mode 100644 index 000000000..4db1c4e0c --- /dev/null +++ b/src/containers/Tests/index.tsx @@ -0,0 +1,27 @@ +import { createRoot } from "react-dom/client"; +import { + cancelMessage, + initializeDigmaMessageListener, + sendMessage +} from "../../api"; +import { Tests } from "../../components/Tests"; +import { App } from "../../components/common/App"; +import { dispatcher } from "../../dispatcher"; +import { GlobalStyle } from "./styles"; + +initializeDigmaMessageListener(dispatcher); + +window.sendMessageToDigma = sendMessage; +window.cancelMessageToDigma = cancelMessage; + +const rootElement = document.getElementById("root"); + +if (rootElement) { + const root = createRoot(rootElement); + root.render( + + + + + ); +} diff --git a/src/containers/Tests/styles.ts b/src/containers/Tests/styles.ts new file mode 100644 index 000000000..52176c92a --- /dev/null +++ b/src/containers/Tests/styles.ts @@ -0,0 +1,7 @@ +import { createGlobalStyle } from "styled-components"; + +export const GlobalStyle = createGlobalStyle` + body { + background: ${({ theme }) => theme.colors.panel.background}; + } +`; diff --git a/src/globals.d.ts b/src/globals.d.ts index b307fc97f..65e52ea09 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -50,6 +50,7 @@ declare global { recentActivityExpirationLimit?: unknown; recentActivityDocumentationURL?: unknown; recentActivityIsEnvironmentManagementEnabled?: unknown; + testsRefreshInterval?: unknown; wizardSkipInstallationStep?: unknown; wizardFirstLaunch?: unknown; } diff --git a/src/styled.d.ts b/src/styled.d.ts index 3c32f094e..8ad2c9b30 100644 --- a/src/styled.d.ts +++ b/src/styled.d.ts @@ -31,6 +31,9 @@ export interface ThemeColors { attachmentTag: AttachmentTagThemeColors; jiraTicket: JiraTicketThemeColors; field: FieldThemeColors; + panel: { + background: string; + }; } declare module "styled-components" { diff --git a/webpackEntries.ts b/webpackEntries.ts index d99d5c055..c33cd83d6 100644 --- a/webpackEntries.ts +++ b/webpackEntries.ts @@ -43,6 +43,9 @@ export const entries: AppEntries = { "recentActivityIsEnvironmentManagementEnabled" ] }, + tests: { + entry: path.resolve(__dirname, "./src/containers/Tests/index.tsx") + }, troubleshooting: { entry: path.resolve(__dirname, "./src/containers/Troubleshooting/index.tsx") } From 3d9025083513ab3f46e1786c0edbbd4b204fb128 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Mon, 8 Jan 2024 17:02:30 +0100 Subject: [PATCH 02/13] Send get data message on the app's start --- src/components/Tests/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Tests/index.tsx b/src/components/Tests/index.tsx index b2e9c4d36..03ffbfb23 100644 --- a/src/components/Tests/index.tsx +++ b/src/components/Tests/index.tsx @@ -6,7 +6,8 @@ import * as s from "./styles"; const ACTION_PREFIX = "TESTS"; const actions = addPrefix(ACTION_PREFIX, { - INITIALIZE: "INITIALIZE" + INITIALIZE: "INITIALIZE", + SPAN_GET_LATEST_DATA: "SPAN_GET_LATEST_DATA" }); const TRACKING_PREFIX = "tests"; @@ -25,6 +26,10 @@ export const Tests = () => { action: actions.INITIALIZE }); + window.sendMessageToDigma({ + action: actions.SPAN_GET_LATEST_DATA + }); + sendTrackingEvent(trackingEvents.PAGE_LOADED); }, []); From 03a8c49e5db614e55583a0d871f3ce38b80c6c95 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 9 Jan 2024 09:58:26 +0100 Subject: [PATCH 03/13] Add refresh and initial loading --- src/components/Tests/index.tsx | 99 ++++++++++++++++++++++++++++++++-- src/components/Tests/styles.ts | 8 +++ src/components/Tests/types.ts | 35 ++++++++++++ webpackEntries.ts | 3 +- 4 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 src/components/Tests/types.ts diff --git a/src/components/Tests/index.tsx b/src/components/Tests/index.tsx index 03ffbfb23..cfc68132a 100644 --- a/src/components/Tests/index.tsx +++ b/src/components/Tests/index.tsx @@ -1,13 +1,25 @@ -import { useEffect } from "react"; +import { useEffect, useRef, useState } from "react"; +import { dispatcher } from "../../dispatcher"; +import { usePrevious } from "../../hooks/usePrevious"; +import { isNumber } from "../../typeGuards/isNumber"; import { addPrefix } from "../../utils/addPrefix"; import { sendTrackingEvent } from "../../utils/sendTrackingEvent"; +import { EmptyState } from "../common/EmptyState"; +import { NewCircleLoader } from "../common/NewCircleLoader"; import * as s from "./styles"; +import { SetSpanLatestDataPayload } from "./types"; + +const PAGE_SIZE = 10; +const REFRESH_INTERVAL = isNumber(window.testsRefreshInterval) + ? window.testsRefreshInterval + : 10 * 1000; // in milliseconds const ACTION_PREFIX = "TESTS"; const actions = addPrefix(ACTION_PREFIX, { INITIALIZE: "INITIALIZE", - SPAN_GET_LATEST_DATA: "SPAN_GET_LATEST_DATA" + GET_SPAN_LATEST_DATA: "SPAN_GET_LATEST_DATA", + SET_SPAN_LATEST_DATA: "SPAN_SET_LATEST_DATA" }); const TRACKING_PREFIX = "tests"; @@ -21,17 +33,94 @@ export const trackingEvents = addPrefix( ); export const Tests = () => { + const [data, setData] = useState(); + const previousData = usePrevious(data); + const [page, setPage] = useState(0); + const previousPage = usePrevious(page); + const refreshTimerId = useRef(); + const [isInitialLoading, setIsInitialLoading] = useState(false); + const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); + const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); + useEffect(() => { window.sendMessageToDigma({ action: actions.INITIALIZE }); + sendTrackingEvent(trackingEvents.PAGE_LOADED); + window.sendMessageToDigma({ - action: actions.SPAN_GET_LATEST_DATA + action: actions.GET_SPAN_LATEST_DATA, + payload: { + page: 0, + pageSize: PAGE_SIZE + } }); + setIsInitialLoading(true); - sendTrackingEvent(trackingEvents.PAGE_LOADED); + const handleSetSpanLatestData = (data: unknown, timeStamp: number) => { + setData(data as SetSpanLatestDataPayload); + setLastSetDataTimeStamp(timeStamp); + }; + + dispatcher.addActionListener( + actions.SET_SPAN_LATEST_DATA, + handleSetSpanLatestData + ); + + return () => { + dispatcher.removeActionListener( + actions.SET_SPAN_LATEST_DATA, + handleSetSpanLatestData + ); + window.clearTimeout(refreshTimerId.current); + }; }, []); - return Tests; + useEffect(() => { + if (previousLastSetDataTimeStamp !== lastSetDataTimeStamp) { + window.clearTimeout(refreshTimerId.current); + refreshTimerId.current = window.setTimeout(() => { + window.sendMessageToDigma({ + action: actions.GET_SPAN_LATEST_DATA, + payload: { + pageNumber: page + 1, + pageSize: PAGE_SIZE + } + }); + }, REFRESH_INTERVAL); + } + }, [previousLastSetDataTimeStamp, lastSetDataTimeStamp, page]); + + useEffect(() => { + if (isNumber(previousPage) && previousPage !== page) { + window.sendMessageToDigma({ + action: actions.GET_SPAN_LATEST_DATA, + payload: { + pageNumber: page, + pageSize: PAGE_SIZE + } + }); + } + }, [previousPage, page]); + + useEffect(() => { + if (!previousData && data) { + setIsInitialLoading(false); + } + }, [previousData, data]); + + const renderContent = () => { + if (isInitialLoading) { + return ( + + } /> + + ); + } + + return JSON.stringify(data); + }; + + return {renderContent()}; }; diff --git a/src/components/Tests/styles.ts b/src/components/Tests/styles.ts index 5b0057276..59ebc991a 100644 --- a/src/components/Tests/styles.ts +++ b/src/components/Tests/styles.ts @@ -3,4 +3,12 @@ import styled from "styled-components"; export const Container = styled.div` background: ${({ theme }) => theme.colors.panel.background}; min-height: 100vh; + display: flex; +`; + +export const NoDataContainer = styled.div` + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; `; diff --git a/src/components/Tests/types.ts b/src/components/Tests/types.ts new file mode 100644 index 000000000..356f0277c --- /dev/null +++ b/src/components/Tests/types.ts @@ -0,0 +1,35 @@ +import { Duration } from "../../globals"; +import { InsightType } from "../../types"; + +export interface SetSpanLatestDataPayload { + data: { + spanContexts: { + spanInfo: { + displayName: string; + spanCodeObjectId: string; + methodCodeObjectId?: string; + }; + tests: { + name: string; + spanInfo: { + displayName: string; + spanCodeObjectId: string; + methodCodeObjectId?: string; + }; + result: "success" | "fail" | "error"; + runAt: string; + duration: Duration; + environment: string; + errorOrFailMessage?: string; + traceId: string; + commitId?: string; + ticketId?: string; + insights: InsightType[]; + contextsSpanCodeObjectIds: string[]; + }[]; + }[]; + }; + error?: { + message: string; + }; +} diff --git a/webpackEntries.ts b/webpackEntries.ts index c33cd83d6..7c9457cc7 100644 --- a/webpackEntries.ts +++ b/webpackEntries.ts @@ -44,7 +44,8 @@ export const entries: AppEntries = { ] }, tests: { - entry: path.resolve(__dirname, "./src/containers/Tests/index.tsx") + entry: path.resolve(__dirname, "./src/containers/Tests/index.tsx"), + environmentVariables: ["testsRefreshInterval"] }, troubleshooting: { entry: path.resolve(__dirname, "./src/containers/Troubleshooting/index.tsx") From d4a3ebdfabd3126171dad5294c2cca749a79f9bc Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 9 Jan 2024 13:17:28 +0100 Subject: [PATCH 04/13] Change page field name --- src/components/Tests/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tests/index.tsx b/src/components/Tests/index.tsx index cfc68132a..34016d75f 100644 --- a/src/components/Tests/index.tsx +++ b/src/components/Tests/index.tsx @@ -52,7 +52,7 @@ export const Tests = () => { window.sendMessageToDigma({ action: actions.GET_SPAN_LATEST_DATA, payload: { - page: 0, + pageNumber: 0, pageSize: PAGE_SIZE } }); From fac90e40b520d360259d8637b73d47cb7a4926ef Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 16 Jan 2024 08:13:54 +0100 Subject: [PATCH 05/13] Add Environment filter, update styles --- src/actions.ts | 4 +- src/components/Insights/JiraTicket/index.tsx | 4 +- src/components/Insights/JiraTicket/types.ts | 2 +- .../Tests/EnvironmentFilter/index.tsx | 62 +++++ .../Tests/EnvironmentFilter/styles.ts | 72 +++++ .../Tests/EnvironmentFilter/types.ts | 11 + .../Tests/TestCard/TestCard.stories.tsx | 49 ++++ src/components/Tests/TestCard/index.tsx | 157 +++++++++++ src/components/Tests/TestCard/mockData.ts | 31 +++ src/components/Tests/TestCard/styles.ts | 63 +++++ src/components/Tests/TestCard/types.ts | 7 + src/components/Tests/Tests.stories.tsx | 75 +++++- src/components/Tests/actions.ts | 12 + src/components/Tests/index.tsx | 254 +++++++++++++++--- src/components/Tests/styles.ts | 82 +++++- src/components/Tests/types.ts | 67 ++--- src/components/common/App/ConfigContext.ts | 4 +- src/components/common/App/getTheme.ts | 51 +++- src/components/common/App/index.tsx | 42 ++- src/components/common/App/types.ts | 11 + .../common/icons/12px/TimerIcon.tsx | 34 +++ .../common/icons/16px/TimerIcon.tsx | 34 +++ src/styled.d.ts | 24 +- 23 files changed, 1078 insertions(+), 74 deletions(-) create mode 100644 src/components/Tests/EnvironmentFilter/index.tsx create mode 100644 src/components/Tests/EnvironmentFilter/styles.ts create mode 100644 src/components/Tests/EnvironmentFilter/types.ts create mode 100644 src/components/Tests/TestCard/TestCard.stories.tsx create mode 100644 src/components/Tests/TestCard/index.tsx create mode 100644 src/components/Tests/TestCard/mockData.ts create mode 100644 src/components/Tests/TestCard/styles.ts create mode 100644 src/components/Tests/TestCard/types.ts create mode 100644 src/components/Tests/actions.ts create mode 100644 src/components/common/icons/12px/TimerIcon.tsx create mode 100644 src/components/common/icons/16px/TimerIcon.tsx diff --git a/src/actions.ts b/src/actions.ts index 84abdb4dc..65e42865f 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -25,5 +25,7 @@ export const actions = addPrefix(ACTION_PREFIX, { SET_OBSERVABILITY: "SET_OBSERVABILITY", GET_BACKEND_INFO: "GET_BACKEND_INFO", SET_BACKEND_INFO: "SET_BACKEND_INFO", - REGISTER: "REGISTER" + REGISTER: "REGISTER", + SET_ENVIRONMENTS: "SET_ENVIRONMENTS", + SET_SELECTED_CODE_SCOPE: "SET_SELECTED_CODE_SCOPE" }); diff --git a/src/components/Insights/JiraTicket/index.tsx b/src/components/Insights/JiraTicket/index.tsx index 23ab084cf..0e320b260 100644 --- a/src/components/Insights/JiraTicket/index.tsx +++ b/src/components/Insights/JiraTicket/index.tsx @@ -53,7 +53,7 @@ export const JiraTicket = (props: JiraTicketProps) => { value: HTMLElement | null | string ) => { sendTrackingEvent(trackingEvents.JIRA_TICKET_FIELD_COPY_BUTTON_CLICKED, { - insightType: props.insight.type, + ...(props.insight ? { insightType: props.insight.type } : {}), field }); @@ -72,7 +72,7 @@ export const JiraTicket = (props: JiraTicketProps) => { sendTrackingEvent( trackingEvents.JIRA_TICKET_ATTACHMENT_DOWNLOAD_BUTTON_CLICKED, { - insightType: props.insight.type + ...(props.insight ? { insightType: props.insight.type } : {}) } ); diff --git a/src/components/Insights/JiraTicket/types.ts b/src/components/Insights/JiraTicket/types.ts index e02b177ec..7f8957c10 100644 --- a/src/components/Insights/JiraTicket/types.ts +++ b/src/components/Insights/JiraTicket/types.ts @@ -19,6 +19,6 @@ export interface JiraTicketProps { errorMessage?: string; }; attachment?: { url: string; fileName: string }; - insight: GenericCodeObjectInsight; + insight?: GenericCodeObjectInsight; onClose: () => void; } diff --git a/src/components/Tests/EnvironmentFilter/index.tsx b/src/components/Tests/EnvironmentFilter/index.tsx new file mode 100644 index 000000000..5d7e2d6f8 --- /dev/null +++ b/src/components/Tests/EnvironmentFilter/index.tsx @@ -0,0 +1,62 @@ +import { useState } from "react"; +import { FilterMenu } from "../../Assets/FilterMenu"; +import { NewPopover } from "../../common/NewPopover"; +import { ChevronIcon } from "../../common/icons/ChevronIcon"; +import { GlobeIcon } from "../../common/icons/GlobeIcon"; +import { Direction } from "../../common/icons/types"; +import * as s from "./styles"; +import { EnvironmentFilterProps } from "./types"; + +export const EnvironmentFilter = (props: EnvironmentFilterProps) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const handleMenuItemClick = (value: string) => { + props.onMenuItemClick(value); + }; + + const handleServiceMenuClose = () => { + setIsMenuOpen(false); + }; + + const selectedItems = props.items.filter((x) => x.selected); + + return ( + + } + onOpenChange={setIsMenuOpen} + isOpen={isMenuOpen} + placement={"bottom-start"} + > + + + + + + Environment : + {selectedItems && selectedItems.length > 0 && !props.isLoading ? ( + {selectedItems.length} + ) : ( + + All + + )} + + + + + + + ); +}; diff --git a/src/components/Tests/EnvironmentFilter/styles.ts b/src/components/Tests/EnvironmentFilter/styles.ts new file mode 100644 index 000000000..592b7d942 --- /dev/null +++ b/src/components/Tests/EnvironmentFilter/styles.ts @@ -0,0 +1,72 @@ +import styled from "styled-components"; +import { MenuButtonProps } from "./types"; + +export const MenuButton = styled.button` + border: 1px solid + ${({ theme, $isOpen }) => + $isOpen ? theme.colors.stroke.brand : theme.colors.stroke.primary}; + background: ${({ theme }) => theme.colors.surface.secondary}; + border-radius: 4px; + padding: 4px 6px 4px 4px; + display: flex; + gap: 10px; + align-items: center; + + &:hover { + border: 1px solid ${({ theme }) => theme.colors.stroke.secondary}; + } + + &:focus, + &:active { + border: 1px solid ${({ theme }) => theme.colors.stroke.brand}; + } +`; + +export const MenuButtonLabel = styled.span` + display: flex; + gap: 4px; + align-items: center; + font-size: 14px; + color: ${({ theme }) => theme.colors.text.base}; +`; + +export const IconContainer = styled.span` + display: flex; + padding: 2px; + border-radius: 4px; + background: ${({ theme }) => theme.colors.surface.brand}; + color: ${({ theme }) => theme.colors.icon.white}; +`; + +export const SelectedEntriesNumberPlaceholder = styled.span` + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; + user-select: none; +`; + +export const Number = styled.span` + min-width: 18px; + height: 18px; + flex-shrink: 0; + font-size: 14px; + line-height: 100%; + font-weight: 500; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + background: #5053d4; +`; + +export const MenuChevronIconContainer = styled.span` + margin-left: auto; + color: ${({ theme }) => theme.colors.icon.primary}; +`; diff --git a/src/components/Tests/EnvironmentFilter/types.ts b/src/components/Tests/EnvironmentFilter/types.ts new file mode 100644 index 000000000..5bd73c00a --- /dev/null +++ b/src/components/Tests/EnvironmentFilter/types.ts @@ -0,0 +1,11 @@ +import { MenuItem } from "../../Assets/FilterMenu/types"; + +export interface EnvironmentFilterProps { + items: MenuItem[]; + onMenuItemClick: (value: string) => void; + isLoading: boolean; +} + +export interface MenuButtonProps { + $isOpen: boolean; +} diff --git a/src/components/Tests/TestCard/TestCard.stories.tsx b/src/components/Tests/TestCard/TestCard.stories.tsx new file mode 100644 index 000000000..f22ab8ebe --- /dev/null +++ b/src/components/Tests/TestCard/TestCard.stories.tsx @@ -0,0 +1,49 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { TestCard } from "."; +import { mockedTest } from "./mockData"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Tests/TestCard", + component: TestCard, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Passed: Story = { + args: { + spanContexts: [ + { + displayName: "spanDisplayName", + spanCodeObjectId: "123", + methodCodeObjectId: "methodCodeObjectId123" + } + ], + test: mockedTest + } +}; + +export const Failed: Story = { + args: { + spanContexts: [ + { + displayName: "spanDisplayName", + spanCodeObjectId: "123", + methodCodeObjectId: "methodCodeObjectId123" + } + ], + test: { + ...mockedTest, + result: "fail", + errorOrFailMessage: "Assertion error message" + } + } +}; diff --git a/src/components/Tests/TestCard/index.tsx b/src/components/Tests/TestCard/index.tsx new file mode 100644 index 000000000..067d4d8d3 --- /dev/null +++ b/src/components/Tests/TestCard/index.tsx @@ -0,0 +1,157 @@ +import { isString } from "../../../typeGuards/isString"; +import { formatTimeDistance } from "../../../utils/formatTimeDistance"; +import { NewButton } from "../../common/NewButton"; +import { Tag } from "../../common/Tag"; +import { Tooltip } from "../../common/Tooltip"; +import { TraceIcon } from "../../common/icons/12px/TraceIcon"; +import { JiraLogoIcon } from "../../common/icons/16px/JiraLogoIcon"; +import { TimerIcon } from "../../common/icons/16px/TimerIcon"; +import { CheckmarkCircleInvertedIcon } from "../../common/icons/CheckmarkCircleInvertedIcon"; +import { CrossCircleIcon } from "../../common/icons/CrossCircleIcon"; +import { GlobeIcon } from "../../common/icons/GlobeIcon"; +import { PlayIcon } from "../../common/icons/PlayIcon"; +import { actions } from "../actions"; +import { Test } from "../types"; +import * as s from "./styles"; +import { TestCardProps } from "./types"; + +const renderTestResultTag = (test: Test) => { + switch (test.result) { + case "success": + return ( + + ); + case "fail": + return ( + + ); + case "error": + return ( + + ); + } +}; + +export const TestCard = (props: TestCardProps) => { + const handleTestNameClick = () => { + window.sendMessageToDigma({ + action: actions.GO_TO_SPAN_OF_TEST, + payload: { + environment: props.test.environmentId, + spanCodeObjectId: props.test.spanInfo.spanCodeObjectId, + methodCodeObjectId: props.test.spanInfo.methodCodeObjectId + } + }); + }; + + const handleTicketButtonClick = () => { + props.onTicketInfoOpen(props.test); + }; + + const handleTraceButtonClick = () => { + const spanContext = props.spanContexts.find((context) => { + const id = props.test.contextsSpanCodeObjectIds.find( + (x) => x === context.spanCodeObjectId + ); + + return context.spanCodeObjectId === id; + }); + + window.sendMessageToDigma({ + action: actions.GO_TO_TRACE, + payload: { + traceId: props.test.traceId, + displayName: spanContext?.displayName, + spanCodeObjectId: spanContext?.spanCodeObjectId + } + }); + }; + + const handleRunButtonClick = () => { + window.sendMessageToDigma({ + action: actions.RUN_TEST, + payload: { + methodCodeObjectId: props.test.spanInfo.methodCodeObjectId + } + }); + }; + + const durationString = `${props.test.duration.value} ${props.test.duration.unit}`; + + return ( + + + {renderTestResultTag(props.test)} + + + {props.test.name} + + + + + + + + + + {props.test.environment} + + + + + + + + {formatTimeDistance(props.test.runAt)} + + + + Duration + + {durationString} + + + + + + + + + + ); +}; diff --git a/src/components/Tests/TestCard/mockData.ts b/src/components/Tests/TestCard/mockData.ts new file mode 100644 index 000000000..a6adfebad --- /dev/null +++ b/src/components/Tests/TestCard/mockData.ts @@ -0,0 +1,31 @@ +import { Test } from "../types"; + +export const mockedTest: Test = { + name: "GET /owners/{ownerId}/pets/{petId}/visits/new", + spanInfo: { + name: "GET /owners/{ownerId}/pets/{petId}/visits/new", + displayName: "GET /owners/{ownerId}/pets/{petId}/visits/new", + instrumentationLibrary: "com.digma.junit", + spanCodeObjectId: + "span:com.digma.junit$_$GET /owners/{ownerId}/pets/{petId}/visits/new", + methodCodeObjectId: + "org.springframework.samples.petclinic.owner.VisitController$_$initNewVisitForm", + kind: "Internal", + codeObjectId: + "org.springframework.samples.petclinic.owner.VisitController$_$initNewVisitForm" + }, + result: "success", + runAt: "2024-01-04T16:06:46.568728Z", + duration: { + value: 1.11, + unit: "μs", + raw: 1111 + }, + environment: "BOB-MACBOOK-PRO-2.LOCAL[LOCAL-TESTS]", + environmentId: "BOB-MACBOOK-PRO-2.LOCAL[LOCAL-TESTS]#ID#1", + traceId: "E03E928B296A8C69511F09422DE6CDA5", + ticketId: null, + commitId: null, + errorOrFailMessage: null, + contextsSpanCodeObjectIds: ["123"] +}; diff --git a/src/components/Tests/TestCard/styles.ts b/src/components/Tests/TestCard/styles.ts new file mode 100644 index 000000000..a36334d25 --- /dev/null +++ b/src/components/Tests/TestCard/styles.ts @@ -0,0 +1,63 @@ +import styled from "styled-components"; +import { Link } from "../../common/Link"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + background: ${({ theme }) => theme.colors.surface.card}; + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.colors.stroke.primary}; + font-size: 14px; +`; + +export const TestNameLink = styled(Link)` + color: ${({ theme }) => theme.colors.text.link}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const Header = styled.div` + display: flex; + gap: 8px; + align-items: center; + color: ${({ theme }) => theme.colors.text.base}; + font-weight: 500; + border-bottom: 1px solid ${({ theme }) => theme.colors.stroke.primary}; + padding: 8px; +`; + +export const Content = styled.div` + display: flex; + gap: 4px; + padding: 8px; + flex-wrap: wrap; +`; + +export const Stat = styled.div` + display: flex; + align-items: center; + gap: 4px; + padding: 4px; + max-width: 150px; + color: ${({ theme }) => theme.colors.text.subtext}; +`; + +export const IconContainer = styled.div` + display: flex; + color: ${({ theme }) => theme.colors.icon.disabledAlt}; +`; + +export const StatValue = styled.span` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const ButtonsContainer = styled.div` + display: flex; + gap: 8px; + align-items: center; + margin-left: auto; + margin-top: auto; +`; diff --git a/src/components/Tests/TestCard/types.ts b/src/components/Tests/TestCard/types.ts new file mode 100644 index 000000000..70c4f135e --- /dev/null +++ b/src/components/Tests/TestCard/types.ts @@ -0,0 +1,7 @@ +import { Test, TestsData } from "../types"; + +export interface TestCardProps { + test: Test; + spanContexts: TestsData["spanContexts"]; + onTicketInfoOpen: (test: Test) => void; +} diff --git a/src/components/Tests/Tests.stories.tsx b/src/components/Tests/Tests.stories.tsx index 9ad7e31b7..263e31335 100644 --- a/src/components/Tests/Tests.stories.tsx +++ b/src/components/Tests/Tests.stories.tsx @@ -1,6 +1,7 @@ import { Meta, StoryObj } from "@storybook/react"; import { Tests } from "."; +import { mockedTest } from "./TestCard/mockData"; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction const meta: Meta = { @@ -17,4 +18,76 @@ export default meta; type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args -export const Default: Story = {}; +export const Default: Story = { + args: { + data: { + data: { + paging: { + pageNumber: 1, + pageSize: 10, + totalCount: 12 + }, + spanContexts: [ + { + displayName: "spanDisplayName", + spanCodeObjectId: "123", + methodCodeObjectId: "methodCodeObjectId123" + } + ], + entries: [ + { ...mockedTest, name: "Test 1" }, + { ...mockedTest, name: "Test 2" }, + { ...mockedTest, name: "Test 3" }, + { ...mockedTest, name: "Test 4" }, + { ...mockedTest, name: "Test 5" }, + { ...mockedTest, name: "Test 6" }, + { ...mockedTest, name: "Test 7" }, + { ...mockedTest, name: "Test 8" }, + { ...mockedTest, name: "Test 9" }, + { ...mockedTest, name: "Test 10" }, + { ...mockedTest, name: "Test 11" }, + { ...mockedTest, name: "Test 12" } + ] + }, + error: null + } + } +}; + +export const Empty: Story = { + args: { + data: { + data: { + paging: { + pageNumber: 1, + pageSize: 10, + totalCount: 0 + }, + spanContexts: [ + { + displayName: "spanDisplayName", + spanCodeObjectId: "123", + methodCodeObjectId: "methodCodeObjectId123" + } + ], + entries: [] + }, + error: null + } + } +}; + +export const Error: Story = { + args: { + data: { + data: null, + error: { + message: "Error message" + } + } + } +}; + +export const Loading: Story = { + args: {} +}; diff --git a/src/components/Tests/actions.ts b/src/components/Tests/actions.ts new file mode 100644 index 000000000..9c24358ff --- /dev/null +++ b/src/components/Tests/actions.ts @@ -0,0 +1,12 @@ +import { addPrefix } from "../../utils/addPrefix"; + +const ACTION_PREFIX = "TESTS"; + +export const actions = addPrefix(ACTION_PREFIX, { + INITIALIZE: "INITIALIZE", + GET_SPAN_LATEST_DATA: "SPAN_GET_LATEST_DATA", + SET_SPAN_LATEST_DATA: "SPAN_SET_LATEST_DATA", + RUN_TEST: "RUN_TEST", + GO_TO_TRACE: "GO_TO_TRACE", + GO_TO_SPAN_OF_TEST: "GO_TO_SPAN_OF_TEST" +}); diff --git a/src/components/Tests/index.tsx b/src/components/Tests/index.tsx index 34016d75f..00d7982be 100644 --- a/src/components/Tests/index.tsx +++ b/src/components/Tests/index.tsx @@ -1,27 +1,28 @@ -import { useEffect, useRef, useState } from "react"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; +import { actions as globalActions } from "../../actions"; import { dispatcher } from "../../dispatcher"; import { usePrevious } from "../../hooks/usePrevious"; import { isNumber } from "../../typeGuards/isNumber"; import { addPrefix } from "../../utils/addPrefix"; import { sendTrackingEvent } from "../../utils/sendTrackingEvent"; -import { EmptyState } from "../common/EmptyState"; +import { MenuItem } from "../Assets/FilterMenu/types"; +import { JiraTicket } from "../Insights/JiraTicket"; +import { ConfigContext } from "../common/App/ConfigContext"; import { NewCircleLoader } from "../common/NewCircleLoader"; +import { Pagination } from "../common/Pagination"; +import { RegistrationDialog } from "../common/RegistrationDialog"; +import { RegistrationFormValues } from "../common/RegistrationDialog/types"; +import { EnvironmentFilter } from "./EnvironmentFilter"; +import { TestCard } from "./TestCard"; +import { actions } from "./actions"; import * as s from "./styles"; -import { SetSpanLatestDataPayload } from "./types"; +import { SetSpanLatestDataPayload, Test, TestsProps } from "./types"; const PAGE_SIZE = 10; const REFRESH_INTERVAL = isNumber(window.testsRefreshInterval) ? window.testsRefreshInterval : 10 * 1000; // in milliseconds -const ACTION_PREFIX = "TESTS"; - -const actions = addPrefix(ACTION_PREFIX, { - INITIALIZE: "INITIALIZE", - GET_SPAN_LATEST_DATA: "SPAN_GET_LATEST_DATA", - SET_SPAN_LATEST_DATA: "SPAN_SET_LATEST_DATA" -}); - const TRACKING_PREFIX = "tests"; export const trackingEvents = addPrefix( @@ -32,29 +33,67 @@ export const trackingEvents = addPrefix( " " ); -export const Tests = () => { +export const Tests = (props: TestsProps) => { const [data, setData] = useState(); const previousData = usePrevious(data); const [page, setPage] = useState(0); - const previousPage = usePrevious(page); const refreshTimerId = useRef(); const [isInitialLoading, setIsInitialLoading] = useState(false); const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); + const config = useContext(ConfigContext); + const [testToOpenTicketPopup, setTestToOpenTicketPopup] = useState(); + const previousUserRegistrationEmail = usePrevious( + config.userRegistrationEmail + ); + useState(false); + const [isRegistrationInProgress, setIsRegistrationInProgress] = + useState(false); + const [selectedEnvironments, setSelectedEnvironments] = useState( + [] + ); + const testsListRef = useRef(null); + + const totalCount = data?.data?.paging.totalCount || 0; + const pageStartItemNumber = page * PAGE_SIZE + 1; + const pageEndItemNumber = Math.min( + pageStartItemNumber + PAGE_SIZE - 1, + totalCount + ); + + const environmentMenuItems: MenuItem[] = (config.environments || []).map( + (environment) => ({ + value: environment.originalName, + label: environment.name, + selected: selectedEnvironments.includes(environment.originalName) + }) + ); + + const payloadToSend = useMemo( + () => ({ + environments: + selectedEnvironments.length > 0 + ? selectedEnvironments + : (config.environments || []).map((x) => x.originalName), + pageNumber: page + 1 + }), + [page, selectedEnvironments, config.environments] + ); + const previousPayloadToSend = usePrevious(payloadToSend); useEffect(() => { window.sendMessageToDigma({ - action: actions.INITIALIZE + action: actions.INITIALIZE, + payload: { + pageSize: PAGE_SIZE + } }); sendTrackingEvent(trackingEvents.PAGE_LOADED); window.sendMessageToDigma({ action: actions.GET_SPAN_LATEST_DATA, - payload: { - pageNumber: 0, - pageSize: PAGE_SIZE - } + payload: payloadToSend }); setIsInitialLoading(true); @@ -83,26 +122,41 @@ export const Tests = () => { refreshTimerId.current = window.setTimeout(() => { window.sendMessageToDigma({ action: actions.GET_SPAN_LATEST_DATA, - payload: { - pageNumber: page + 1, - pageSize: PAGE_SIZE - } + payload: payloadToSend }); }, REFRESH_INTERVAL); } - }, [previousLastSetDataTimeStamp, lastSetDataTimeStamp, page]); + }, [previousLastSetDataTimeStamp, lastSetDataTimeStamp, payloadToSend]); useEffect(() => { - if (isNumber(previousPage) && previousPage !== page) { + if (previousPayloadToSend && previousPayloadToSend !== payloadToSend) { window.sendMessageToDigma({ action: actions.GET_SPAN_LATEST_DATA, - payload: { - pageNumber: page, - pageSize: PAGE_SIZE - } + payload: payloadToSend }); } - }, [previousPage, page]); + }, [previousPayloadToSend, payloadToSend]); + + useEffect(() => { + if ( + previousUserRegistrationEmail !== config.userRegistrationEmail && + isRegistrationInProgress + ) { + setIsRegistrationInProgress(false); + } + }, [ + config.userRegistrationEmail, + isRegistrationInProgress, + previousUserRegistrationEmail + ]); + + useEffect(() => { + if (!props.data) { + return; + } + + setData(props.data); + }, [props.data]); useEffect(() => { if (!previousData && data) { @@ -110,17 +164,153 @@ export const Tests = () => { } }, [previousData, data]); + useEffect(() => { + setPage(0); + testsListRef.current?.scrollTo(0, 0); + }, [config.scope, selectedEnvironments]); + + const openJiraTicketPopup = (test: Test) => { + setTestToOpenTicketPopup(test); + }; + + const closeJiraTicketPopup = () => { + setTestToOpenTicketPopup(undefined); + }; + + const handleRegistrationSubmit = (formData: RegistrationFormValues) => { + window.sendMessageToDigma({ + action: globalActions.REGISTER, + payload: { + ...formData, + scope: "insights view jira ticket info" + } + }); + + setIsRegistrationInProgress(true); + }; + + const handleRegistrationDialogClose = () => { + setTestToOpenTicketPopup(undefined); + }; + + const handleEnvironmentMenuItemClick = (environment: string) => { + const oldSelectedEnvironments = selectedEnvironments || []; + const environmentIndex = oldSelectedEnvironments.findIndex( + (x) => x === environment + ); + + if (environmentIndex < 0) { + setSelectedEnvironments([...oldSelectedEnvironments, environment]); + } else { + setSelectedEnvironments([ + ...oldSelectedEnvironments.slice(0, environmentIndex), + ...oldSelectedEnvironments.slice(environmentIndex + 1) + ]); + } + }; + const renderContent = () => { if (isInitialLoading) { return ( - } /> + ); } - return JSON.stringify(data); + if (data?.error) { + return {data.error.message}; + } + + if (data?.data?.entries.length === 0) { + return ( + + Run tests with Digma + + Run your test with Digma enabled to see related tests and insights + + + ); + } + + return ( + + + {data?.data?.entries.map((x) => { + const key = `${x.environmentId}-${x.name}`; + return ( + + ); + })} + + + + Showing{" "} + + {pageStartItemNumber} - {pageEndItemNumber} + {" "} + of {totalCount} + + + + + ); }; - return {renderContent()}; + return ( + + + Environment + + + {renderContent()} + {testToOpenTicketPopup && ( + + + {config.userRegistrationEmail ? ( + + testToOpenTicketPopup.contextsSpanCodeObjectIds.includes( + x.spanCodeObjectId + ) + ), + null, + 2 + )}` // TODO: add description + }} + onClose={closeJiraTicketPopup} + /> + ) : ( + + )} + + + )} + + ); }; diff --git a/src/components/Tests/styles.ts b/src/components/Tests/styles.ts index 59ebc991a..01b5bd974 100644 --- a/src/components/Tests/styles.ts +++ b/src/components/Tests/styles.ts @@ -1,9 +1,12 @@ import styled from "styled-components"; +import { LAYERS } from "../common/App/styles"; export const Container = styled.div` background: ${({ theme }) => theme.colors.panel.background}; - min-height: 100vh; + height: 100%; display: flex; + flex-direction: column; + overflow: hidden; `; export const NoDataContainer = styled.div` @@ -11,4 +14,81 @@ export const NoDataContainer = styled.div` display: flex; flex-direction: column; align-items: center; + justify-content: center; + gap: 8px; + text-align: center; + color: ${({ theme }) => theme.colors.text.subtext}; + font-size: 14px; +`; + +export const EnvironmentFilterContainer = styled.div` + display: flex; + flex-direction: column; + padding: 8px 12px; + gap: 8px; + color: ${({ theme }) => theme.colors.text.subtext}; + font-size: 14px; +`; + +export const ContentContainer = styled.div` + display: flex; + flex-direction: column; + height: 100%; + padding: 8px 12px; + gap: 12px; + overflow: auto; +`; + +export const TestsList = styled.div` + display: flex; + flex-direction: column; + overflow: auto; + gap: 12px; +`; + +export const Footer = styled.div` + display: flex; + justify-content: space-between; + font-size: 14px; +`; + +export const ItemsCount = styled.span` + font-weight: 500; + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#818594"; + case "dark": + case "dark-jetbrains": + return "#b4b8bf"; + } + }}; +`; + +export const PageItemsCount = styled.span` + color: ${({ theme }) => { + switch (theme.mode) { + case "light": + return "#494b57"; + case "dark": + case "dark-jetbrains": + return "#dfe1e5"; + } + }}; +`; + +export const Overlay = styled.div` + position: fixed; + inset: 0; + margin: auto; + background: rgb(18 18 21 / 70%); + z-index: ${LAYERS.OVERLAY}; +`; + +export const PopupContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 0 4%; `; diff --git a/src/components/Tests/types.ts b/src/components/Tests/types.ts index 356f0277c..fd68da6cf 100644 --- a/src/components/Tests/types.ts +++ b/src/components/Tests/types.ts @@ -1,35 +1,42 @@ import { Duration } from "../../globals"; -import { InsightType } from "../../types"; +import { SpanInfo } from "../../types"; -export interface SetSpanLatestDataPayload { - data: { - spanContexts: { - spanInfo: { - displayName: string; - spanCodeObjectId: string; - methodCodeObjectId?: string; - }; - tests: { - name: string; - spanInfo: { - displayName: string; - spanCodeObjectId: string; - methodCodeObjectId?: string; - }; - result: "success" | "fail" | "error"; - runAt: string; - duration: Duration; - environment: string; - errorOrFailMessage?: string; - traceId: string; - commitId?: string; - ticketId?: string; - insights: InsightType[]; - contextsSpanCodeObjectIds: string[]; - }[]; - }[]; +export interface Test { + name: string; + spanInfo: SpanInfo; + result: "success" | "fail" | "error"; + runAt: string; + duration: Duration; + environment: string; + environmentId: string; + errorOrFailMessage: string | null; + traceId: string; + commitId: string | null; + ticketId: string | null; + contextsSpanCodeObjectIds: string[]; +} + +export interface TestsData { + paging: { + pageNumber: number; + pageSize: number; + totalCount: number; }; - error?: { + spanContexts: { + displayName: string; + spanCodeObjectId: string; + methodCodeObjectId: string | null; + }[]; + entries: Test[]; +} + +export interface SetSpanLatestDataPayload { + data: TestsData | null; + error: { message: string; - }; + } | null; +} + +export interface TestsProps { + data?: SetSpanLatestDataPayload; } diff --git a/src/components/common/App/ConfigContext.ts b/src/components/common/App/ConfigContext.ts index 2411e830f..812c1c7c7 100644 --- a/src/components/common/App/ConfigContext.ts +++ b/src/components/common/App/ConfigContext.ts @@ -17,5 +17,7 @@ export const ConfigContext = createContext({ ? window.userRegistrationEmail : "", environment: isString(window.environment) ? window.environment : "", - backendInfo: undefined + backendInfo: undefined, + environments: undefined, + scope: undefined }); diff --git a/src/components/common/App/getTheme.ts b/src/components/common/App/getTheme.ts index 24634e940..2a968c607 100644 --- a/src/components/common/App/getTheme.ts +++ b/src/components/common/App/getTheme.ts @@ -13,6 +13,7 @@ export const grayScale = { 500: "#828599", 600: "#565966", 700: "#4c4e59", + 750: "#37383F", 800: "#2c2e33", 850: "#37383f", 900: "#2b2c33", @@ -57,7 +58,11 @@ export const greenScale = { }; const darkThemeColors: ThemeColors = { - icon: grayScale[200], + icon: { + white: grayScale[0], + primary: grayScale[100], + disabledAlt: grayScale[500] + }, button: { primary: { background: { @@ -237,11 +242,33 @@ const darkThemeColors: ThemeColors = { }, panel: { background: grayScale[1000] + }, + text: { + base: grayScale[0], + subtext: grayScale[400], + link: primaryScale[100], + success: greenScale[500] + }, + surface: { + primaryLight: grayScale[800], + highlight: grayScale[750], + card: grayScale[1100], + brand: primaryScale[300], + secondary: grayScale[1100] + }, + stroke: { + primary: grayScale[750], + secondary: grayScale[500], + brand: primaryScale[300] } }; const lightThemeColors: ThemeColors = { - icon: grayScale[800], + icon: { + white: grayScale[0], + primary: grayScale[800], + disabledAlt: grayScale[500] + }, button: { primary: { background: { @@ -419,7 +446,25 @@ const lightThemeColors: ThemeColors = { icon: grayScale[800], text: grayScale[800] }, - panel: { background: grayScale[150] } + panel: { background: grayScale[150] }, + text: { + base: grayScale[900], + subtext: grayScale[600], + link: primaryScale[300], + success: grayScale[900] + }, + surface: { + primaryLight: grayScale[50], + highlight: grayScale[150], + card: grayScale[0], + brand: primaryScale[300], + secondary: grayScale[50] + }, + stroke: { + primary: grayScale[500], + secondary: grayScale[800], + brand: primaryScale[300] + } }; const getColors = (mode: Mode): ThemeColors => { diff --git a/src/components/common/App/index.tsx b/src/components/common/App/index.tsx index c71175951..cfdc9869f 100644 --- a/src/components/common/App/index.tsx +++ b/src/components/common/App/index.tsx @@ -10,7 +10,13 @@ import { isString } from "../../../typeGuards/isString"; import { ConfigContext } from "./ConfigContext"; import { getTheme } from "./getTheme"; import { GlobalStyle } from "./styles"; -import { AppProps, BackendInfo, DigmaStatus } from "./types"; +import { + AppProps, + BackendInfo, + DigmaStatus, + Environment, + Scope +} from "./types"; export const THEMES = ["light", "dark", "dark-jetbrains"]; @@ -181,6 +187,24 @@ export const App = (props: AppProps) => { } }; + const handleSetEnvironments = (data: unknown) => { + if (isObject(data) && Array.isArray(data.environments)) { + setConfig((config) => ({ + ...config, + environments: data.environments as Environment[] + })); + } + }; + + const handleSetSelectedCodeScope = (data: unknown) => { + if (isObject(data) && isObject(data.scope) && isString(data.scope.type)) { + setConfig((config) => ({ + ...config, + scope: data.scope as Scope + })); + } + }; + dispatcher.addActionListener(actions.SET_THEME, handleSetTheme); dispatcher.addActionListener(actions.SET_MAIN_FONT, handleSetMainFont); dispatcher.addActionListener(actions.SET_CODE_FONT, handleSetCodeFont); @@ -226,6 +250,14 @@ export const App = (props: AppProps) => { actions.SET_BACKEND_INFO, handleSetBackendInfo ); + dispatcher.addActionListener( + actions.SET_ENVIRONMENTS, + handleSetEnvironments + ); + dispatcher.addActionListener( + actions.SET_SELECTED_CODE_SCOPE, + handleSetSelectedCodeScope + ); return () => { dispatcher.removeActionListener(actions.SET_THEME, handleSetTheme); @@ -279,6 +311,14 @@ export const App = (props: AppProps) => { actions.SET_BACKEND_INFO, handleSetBackendInfo ); + dispatcher.removeActionListener( + actions.SET_ENVIRONMENTS, + handleSetEnvironments + ); + dispatcher.removeActionListener( + actions.SET_SELECTED_CODE_SCOPE, + handleSetSelectedCodeScope + ); }; }, []); diff --git a/src/components/common/App/types.ts b/src/components/common/App/types.ts index 78d3c27c7..798d6e5f0 100644 --- a/src/components/common/App/types.ts +++ b/src/components/common/App/types.ts @@ -28,6 +28,15 @@ export enum DeploymentType { DOCKER_EXTENSION = "DockerExtension" } +export interface Environment { + originalName: string; + name: string; +} + +export interface Scope { + type: string; +} + export interface ConfigContextData { digmaApiUrl: string; digmaStatus: DigmaStatus | undefined; @@ -42,4 +51,6 @@ export interface ConfigContextData { userRegistrationEmail: string; environment: string; backendInfo: BackendInfo | undefined; + environments: Environment[] | undefined; + scope: Scope | undefined; } diff --git a/src/components/common/icons/12px/TimerIcon.tsx b/src/components/common/icons/12px/TimerIcon.tsx new file mode 100644 index 000000000..04199ea7b --- /dev/null +++ b/src/components/common/icons/12px/TimerIcon.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { useIconProps } from "../hooks"; +import { IconProps } from "../types"; + +const TimerIconComponent = (props: IconProps) => { + const { size, color } = useIconProps(props); + + return ( + + + + + + + + + + + + ); +}; + +export const TimerIcon = React.memo(TimerIconComponent); diff --git a/src/components/common/icons/16px/TimerIcon.tsx b/src/components/common/icons/16px/TimerIcon.tsx new file mode 100644 index 000000000..f2a6b54da --- /dev/null +++ b/src/components/common/icons/16px/TimerIcon.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { useIconProps } from "../hooks"; +import { IconProps } from "../types"; + +const TimerIconComponent = (props: IconProps) => { + const { size, color } = useIconProps(props); + + return ( + + + + + + + + + + + + ); +}; + +export const TimerIcon = React.memo(TimerIconComponent); diff --git a/src/styled.d.ts b/src/styled.d.ts index 8ad2c9b30..bd527b23e 100644 --- a/src/styled.d.ts +++ b/src/styled.d.ts @@ -12,7 +12,11 @@ import { TooltipThemeColors } from "./components/common/Tooltip/types"; import { Mode } from "./globals"; export interface ThemeColors { - icon: string; + icon: { + white: string; + primary: string; + disabledAlt: string; + }; button: { primary: ButtonThemeColors; secondary: ButtonThemeColors; @@ -34,6 +38,24 @@ export interface ThemeColors { panel: { background: string; }; + text: { + base: string; + link: string; + subtext: string; + success: string; + }; + surface: { + primaryLight: string; + highlight: string; + card: string; + brand: string; + secondary: string; + }; + stroke: { + primary: string; + secondary: string; + brand: string; + }; } declare module "styled-components" { From 3437b128f4e3ca89f46ca8f6ecbc3a8e87861f36 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 16 Jan 2024 08:21:13 +0100 Subject: [PATCH 06/13] Fix Jira button state --- src/components/Tests/TestCard/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tests/TestCard/index.tsx b/src/components/Tests/TestCard/index.tsx index 067d4d8d3..c9bfb6ff5 100644 --- a/src/components/Tests/TestCard/index.tsx +++ b/src/components/Tests/TestCard/index.tsx @@ -136,7 +136,7 @@ export const TestCard = (props: TestCardProps) => { onClick={handleTicketButtonClick} icon={JiraLogoIcon} buttonType={"tertiary"} - disabled={["fail", "error"].includes(props.test.result)} + disabled={props.test.result === "success"} /> Date: Tue, 16 Jan 2024 08:31:52 +0100 Subject: [PATCH 07/13] Update styles --- src/components/Tests/TestCard/index.tsx | 64 ++++++++++++----------- src/components/common/NewButton/index.tsx | 9 +++- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/components/Tests/TestCard/index.tsx b/src/components/Tests/TestCard/index.tsx index c9bfb6ff5..1050c5dd7 100644 --- a/src/components/Tests/TestCard/index.tsx +++ b/src/components/Tests/TestCard/index.tsx @@ -109,40 +109,44 @@ export const TestCard = (props: TestCardProps) => { - - - - - + + + + + {props.test.environment} - - - - - - - + + + + + + + {formatTimeDistance(props.test.runAt)} - - - - Duration - + + + + + Duration {durationString} - - + + - - + + + + + + { +export const NewButtonComponent = ( + props: NewButtonProps, + ref: ForwardedRef +) => { const buttonType = props.buttonType || "primary"; const buttonSize = props.size || "small"; const iconSize = buttonSize === "large" ? 16 : 13; return ( { ); }; + +export const NewButton = forwardRef(NewButtonComponent); From c61a98988c190192836554cf25f35e5a21a6ff4f Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 16 Jan 2024 08:34:15 +0100 Subject: [PATCH 08/13] Update axios --- package-lock.json | 30 +++++++++++++++--------------- package.json | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99ca29309..715407e92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@floating-ui/react": "^0.25.1", "@tanstack/react-table": "^8.7.8", "allotment": "^1.19.0", - "axios": "^1.6.1", + "axios": "^1.6.5", "copy-to-clipboard": "^3.3.3", "date-fns": "^2.29.3", "free-email-domains": "^1.2.5", @@ -7968,11 +7968,11 @@ } }, "node_modules/axios": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", - "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -11464,9 +11464,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -24613,11 +24613,11 @@ "dev": true }, "axios": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", - "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -27237,9 +27237,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" }, "for-each": { "version": "0.3.3", diff --git a/package.json b/package.json index 9390a4eaf..06e34c7ed 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "@floating-ui/react": "^0.25.1", "@tanstack/react-table": "^8.7.8", "allotment": "^1.19.0", - "axios": "^1.6.1", + "axios": "^1.6.5", "copy-to-clipboard": "^3.3.3", "date-fns": "^2.29.3", "free-email-domains": "^1.2.5", From f8b5b523c6d8a986caaf66ab630afa874b4376e7 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 16 Jan 2024 17:55:13 +0100 Subject: [PATCH 09/13] Add analytics, format ticket content --- src/components/Insights/JiraTicket/index.tsx | 4 +- src/components/Insights/JiraTicket/types.ts | 2 +- src/components/Tests/TestCard/index.tsx | 6 + src/components/Tests/TestTicket/index.tsx | 39 ++++ src/components/Tests/TestTicket/types.ts | 7 + src/components/Tests/Tests.stories.tsx | 2 +- src/components/Tests/index.tsx | 34 +--- src/components/Tests/tracking.ts | 15 ++ .../common/JiraTicket/AttachmentTag/index.tsx | 14 ++ .../common/JiraTicket/AttachmentTag/styles.ts | 27 +++ .../common/JiraTicket/AttachmentTag/types.ts | 17 ++ .../common/JiraTicket/Field/index.tsx | 49 +++++ .../common/JiraTicket/Field/styles.ts | 65 +++++++ .../common/JiraTicket/Field/types.ts | 26 +++ .../common/JiraTicket/IconButton/index.tsx | 11 ++ .../common/JiraTicket/IconButton/styles.ts | 11 ++ .../common/JiraTicket/IconButton/types.ts | 9 + .../common/JiraTicket/JiraTicket.stories.tsx | 24 +++ src/components/common/JiraTicket/index.tsx | 174 ++++++++++++++++++ src/components/common/JiraTicket/styles.ts | 49 +++++ src/components/common/JiraTicket/tracking.ts | 6 + src/components/common/JiraTicket/types.ts | 26 +++ 22 files changed, 584 insertions(+), 33 deletions(-) create mode 100644 src/components/Tests/TestTicket/index.tsx create mode 100644 src/components/Tests/TestTicket/types.ts create mode 100644 src/components/Tests/tracking.ts create mode 100644 src/components/common/JiraTicket/AttachmentTag/index.tsx create mode 100644 src/components/common/JiraTicket/AttachmentTag/styles.ts create mode 100644 src/components/common/JiraTicket/AttachmentTag/types.ts create mode 100644 src/components/common/JiraTicket/Field/index.tsx create mode 100644 src/components/common/JiraTicket/Field/styles.ts create mode 100644 src/components/common/JiraTicket/Field/types.ts create mode 100644 src/components/common/JiraTicket/IconButton/index.tsx create mode 100644 src/components/common/JiraTicket/IconButton/styles.ts create mode 100644 src/components/common/JiraTicket/IconButton/types.ts create mode 100644 src/components/common/JiraTicket/JiraTicket.stories.tsx create mode 100644 src/components/common/JiraTicket/index.tsx create mode 100644 src/components/common/JiraTicket/styles.ts create mode 100644 src/components/common/JiraTicket/tracking.ts create mode 100644 src/components/common/JiraTicket/types.ts diff --git a/src/components/Insights/JiraTicket/index.tsx b/src/components/Insights/JiraTicket/index.tsx index 0e320b260..23ab084cf 100644 --- a/src/components/Insights/JiraTicket/index.tsx +++ b/src/components/Insights/JiraTicket/index.tsx @@ -53,7 +53,7 @@ export const JiraTicket = (props: JiraTicketProps) => { value: HTMLElement | null | string ) => { sendTrackingEvent(trackingEvents.JIRA_TICKET_FIELD_COPY_BUTTON_CLICKED, { - ...(props.insight ? { insightType: props.insight.type } : {}), + insightType: props.insight.type, field }); @@ -72,7 +72,7 @@ export const JiraTicket = (props: JiraTicketProps) => { sendTrackingEvent( trackingEvents.JIRA_TICKET_ATTACHMENT_DOWNLOAD_BUTTON_CLICKED, { - ...(props.insight ? { insightType: props.insight.type } : {}) + insightType: props.insight.type } ); diff --git a/src/components/Insights/JiraTicket/types.ts b/src/components/Insights/JiraTicket/types.ts index 7f8957c10..e02b177ec 100644 --- a/src/components/Insights/JiraTicket/types.ts +++ b/src/components/Insights/JiraTicket/types.ts @@ -19,6 +19,6 @@ export interface JiraTicketProps { errorMessage?: string; }; attachment?: { url: string; fileName: string }; - insight?: GenericCodeObjectInsight; + insight: GenericCodeObjectInsight; onClose: () => void; } diff --git a/src/components/Tests/TestCard/index.tsx b/src/components/Tests/TestCard/index.tsx index 1050c5dd7..7771d4b19 100644 --- a/src/components/Tests/TestCard/index.tsx +++ b/src/components/Tests/TestCard/index.tsx @@ -1,5 +1,6 @@ import { isString } from "../../../typeGuards/isString"; import { formatTimeDistance } from "../../../utils/formatTimeDistance"; +import { sendTrackingEvent } from "../../../utils/sendTrackingEvent"; import { NewButton } from "../../common/NewButton"; import { Tag } from "../../common/Tag"; import { Tooltip } from "../../common/Tooltip"; @@ -11,6 +12,7 @@ import { CrossCircleIcon } from "../../common/icons/CrossCircleIcon"; import { GlobeIcon } from "../../common/icons/GlobeIcon"; import { PlayIcon } from "../../common/icons/PlayIcon"; import { actions } from "../actions"; +import { trackingEvents } from "../tracking"; import { Test } from "../types"; import * as s from "./styles"; import { TestCardProps } from "./types"; @@ -54,6 +56,7 @@ const renderTestResultTag = (test: Test) => { export const TestCard = (props: TestCardProps) => { const handleTestNameClick = () => { + sendTrackingEvent(trackingEvents.TEST_NAME_LINK_CLICKED); window.sendMessageToDigma({ action: actions.GO_TO_SPAN_OF_TEST, payload: { @@ -65,10 +68,12 @@ export const TestCard = (props: TestCardProps) => { }; const handleTicketButtonClick = () => { + sendTrackingEvent(trackingEvents.JIRA_TICKET_INFO_BUTTON_CLICKED); props.onTicketInfoOpen(props.test); }; const handleTraceButtonClick = () => { + sendTrackingEvent(trackingEvents.GO_TO_TRACE_BUTTON_CLICKED); const spanContext = props.spanContexts.find((context) => { const id = props.test.contextsSpanCodeObjectIds.find( (x) => x === context.spanCodeObjectId @@ -88,6 +93,7 @@ export const TestCard = (props: TestCardProps) => { }; const handleRunButtonClick = () => { + sendTrackingEvent(trackingEvents.RUN_TEST_BUTTON_CLICKED); window.sendMessageToDigma({ action: actions.RUN_TEST, payload: { diff --git a/src/components/Tests/TestTicket/index.tsx b/src/components/Tests/TestTicket/index.tsx new file mode 100644 index 000000000..3942da14d --- /dev/null +++ b/src/components/Tests/TestTicket/index.tsx @@ -0,0 +1,39 @@ +import { isString } from "../../../typeGuards/isString"; +import { JiraTicket } from "../../common/JiraTicket"; +import { TestTicketProps } from "./types"; + +export const TestTicket = (props: TestTicketProps) => { + const summary = `"${props.test.name}" test failed`; + + const durationString = `${props.test.duration.value} ${props.test.duration.unit}`; + const relatedSpans = props.spanContexts + .filter((x) => + props.test.contextsSpanCodeObjectIds.includes(x.spanCodeObjectId) + ) + .map((x) => x.displayName) + .join("\n"); + + const description = [ + `${props.test.name}" test failed${ + isString(props.test.errorOrFailMessage) + ? ` with message: ${props.test.errorOrFailMessage}` + : "" + }`, + `Last run at: ${new Date(props.test.runAt).toString()}`, + `Duration: ${durationString}`, + relatedSpans.length > 0 ? `Related spans:\n"${relatedSpans}` : "" + ] + .filter(Boolean) + .join("\n\n"); + + return ( + + ); +}; diff --git a/src/components/Tests/TestTicket/types.ts b/src/components/Tests/TestTicket/types.ts new file mode 100644 index 000000000..7cbbae81d --- /dev/null +++ b/src/components/Tests/TestTicket/types.ts @@ -0,0 +1,7 @@ +import { Test, TestsData } from "../types"; + +export interface TestTicketProps { + test: Test; + spanContexts: TestsData["spanContexts"]; + onClose: () => void; +} diff --git a/src/components/Tests/Tests.stories.tsx b/src/components/Tests/Tests.stories.tsx index 263e31335..9764db579 100644 --- a/src/components/Tests/Tests.stories.tsx +++ b/src/components/Tests/Tests.stories.tsx @@ -35,7 +35,7 @@ export const Default: Story = { } ], entries: [ - { ...mockedTest, name: "Test 1" }, + { ...mockedTest, name: "Test 1", result: "fail" }, { ...mockedTest, name: "Test 2" }, { ...mockedTest, name: "Test 3" }, { ...mockedTest, name: "Test 4" }, diff --git a/src/components/Tests/index.tsx b/src/components/Tests/index.tsx index 00d7982be..e7701bc5f 100644 --- a/src/components/Tests/index.tsx +++ b/src/components/Tests/index.tsx @@ -3,10 +3,8 @@ import { actions as globalActions } from "../../actions"; import { dispatcher } from "../../dispatcher"; import { usePrevious } from "../../hooks/usePrevious"; import { isNumber } from "../../typeGuards/isNumber"; -import { addPrefix } from "../../utils/addPrefix"; import { sendTrackingEvent } from "../../utils/sendTrackingEvent"; import { MenuItem } from "../Assets/FilterMenu/types"; -import { JiraTicket } from "../Insights/JiraTicket"; import { ConfigContext } from "../common/App/ConfigContext"; import { NewCircleLoader } from "../common/NewCircleLoader"; import { Pagination } from "../common/Pagination"; @@ -14,8 +12,10 @@ import { RegistrationDialog } from "../common/RegistrationDialog"; import { RegistrationFormValues } from "../common/RegistrationDialog/types"; import { EnvironmentFilter } from "./EnvironmentFilter"; import { TestCard } from "./TestCard"; +import { TestTicket } from "./TestTicket"; import { actions } from "./actions"; import * as s from "./styles"; +import { trackingEvents } from "./tracking"; import { SetSpanLatestDataPayload, Test, TestsProps } from "./types"; const PAGE_SIZE = 10; @@ -23,16 +23,6 @@ const REFRESH_INTERVAL = isNumber(window.testsRefreshInterval) ? window.testsRefreshInterval : 10 * 1000; // in milliseconds -const TRACKING_PREFIX = "tests"; - -export const trackingEvents = addPrefix( - TRACKING_PREFIX, - { - PAGE_LOADED: "page loaded" - }, - " " -); - export const Tests = (props: TestsProps) => { const [data, setData] = useState(); const previousData = usePrevious(data); @@ -282,23 +272,9 @@ export const Tests = (props: TestsProps) => { {config.userRegistrationEmail ? ( - - testToOpenTicketPopup.contextsSpanCodeObjectIds.includes( - x.spanCodeObjectId - ) - ), - null, - 2 - )}` // TODO: add description - }} + ) : ( diff --git a/src/components/Tests/tracking.ts b/src/components/Tests/tracking.ts new file mode 100644 index 000000000..139e4776b --- /dev/null +++ b/src/components/Tests/tracking.ts @@ -0,0 +1,15 @@ +import { addPrefix } from "../../utils/addPrefix"; + +const TRACKING_PREFIX = "tests"; + +export const trackingEvents = addPrefix( + TRACKING_PREFIX, + { + PAGE_LOADED: "page loaded", + TEST_NAME_LINK_CLICKED: "test name link clicked", + GO_TO_TRACE_BUTTON_CLICKED: "go to trace button clicked", + RUN_TEST_BUTTON_CLICKED: "run test button clicked", + JIRA_TICKET_INFO_BUTTON_CLICKED: "jira ticket info button clicked" + }, + " " +); diff --git a/src/components/common/JiraTicket/AttachmentTag/index.tsx b/src/components/common/JiraTicket/AttachmentTag/index.tsx new file mode 100644 index 000000000..6d55bd2ba --- /dev/null +++ b/src/components/common/JiraTicket/AttachmentTag/index.tsx @@ -0,0 +1,14 @@ +import { Tooltip } from "../../../common/Tooltip"; +import * as s from "./styles"; +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 new file mode 100644 index 000000000..2e072b7bb --- /dev/null +++ b/src/components/common/JiraTicket/AttachmentTag/styles.ts @@ -0,0 +1,27 @@ +import styled from "styled-components"; + +export const Container = styled.div` + display: flex; + padding: 4px 6px 4px 4px; + 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; +`; + +export const IconContainer = styled.div` + padding: 2px; + border-radius: 4px; + color: ${({ theme }) => theme.colors.attachmentTag.icon.stroke}; + background: ${({ theme }) => theme.colors.attachmentTag.icon.background}; + display: flex; +`; + +export const TextContainer = styled.span` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/src/components/common/JiraTicket/AttachmentTag/types.ts b/src/components/common/JiraTicket/AttachmentTag/types.ts new file mode 100644 index 000000000..dd182de52 --- /dev/null +++ b/src/components/common/JiraTicket/AttachmentTag/types.ts @@ -0,0 +1,17 @@ +import { ComponentType } from "react"; +import { IconProps } from "../../../common/icons/types"; + +export interface AttachmentTagThemeColors { + background: string; + border: string; + icon: { + background: string; + stroke: string; + }; + text: string; +} + +export interface AttachmentTagProps { + icon: ComponentType; + text: string; +} diff --git a/src/components/common/JiraTicket/Field/index.tsx b/src/components/common/JiraTicket/Field/index.tsx new file mode 100644 index 000000000..891200984 --- /dev/null +++ b/src/components/common/JiraTicket/Field/index.tsx @@ -0,0 +1,49 @@ +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"; + +export const Field = (props: FieldProps) => { + const scrollbar = useScrollbarSize(); + const contentRef = useRef(null); + const { observe } = useDimensions(); + + const getContentRef = useCallback( + (el: HTMLDivElement | null) => { + observe(el); + contentRef.current = el; + }, + [observe] + ); + + const scrollbarOffset = + contentRef.current && + contentRef.current.scrollHeight > contentRef.current.clientHeight + ? scrollbar.width + : 0; + + const iconPosition: ButtonPosition = + props.multiline === true ? "top" : "center"; + + return ( + + {props.label} + + + {props.content} + + {props.button} + + + + {isString(props.errorMessage) && ( + {props.errorMessage} + )} + + ); +}; diff --git a/src/components/common/JiraTicket/Field/styles.ts b/src/components/common/JiraTicket/Field/styles.ts new file mode 100644 index 000000000..076cedf73 --- /dev/null +++ b/src/components/common/JiraTicket/Field/styles.ts @@ -0,0 +1,65 @@ +import styled from "styled-components"; +import { redScale } from "../../../common/App/getTheme"; +import { ButtonContainerProps, ContentProps } from "./types"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 6px; +`; + +export const Label = styled.label` + color: ${({ theme }) => theme.colors.jiraTicket.text.secondary}; +`; + +export const ContentContainer = styled.div` + display: flex; + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.colors.field.border}; + color: ${({ theme }) => theme.colors.field.text}; + position: relative; +`; + +export const Content = styled.div` + width: 100%; + max-height: 200px; + padding: 6px 28px 6px 8px; + overflow: ${({ $multiline }) => ($multiline ? "auto" : "hidden")}; + white-space: ${({ $multiline }) => ($multiline ? "pre-line" : "nowrap")}; + ${({ $multiline }) => + $multiline ? "word-wrap: break-word" : "text-overflow: ellipsis"}; +`; + +export const ButtonContainer = styled.div` + position: absolute; + right: ${({ $scrollbarOffset }) => $scrollbarOffset + 4}px; + ${({ $position }) => { + switch ($position) { + case "top": + return "top: 4px;"; + case "center": + return ` + top: 0; + bottom: 0; + margin: auto; + height: fit-content; + `; + } + }} +`; + +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 new file mode 100644 index 000000000..d3e635fd4 --- /dev/null +++ b/src/components/common/JiraTicket/Field/types.ts @@ -0,0 +1,26 @@ +import { ReactNode } from "react"; + +export interface FieldThemeColors { + border: string; + icon: string; + text: string; +} + +export type ButtonPosition = "top" | "center"; + +export interface FieldProps { + content: ReactNode; + label: string; + button: ReactNode; + multiline?: boolean; + errorMessage?: string; +} + +export interface ButtonContainerProps { + $position: ButtonPosition; + $scrollbarOffset: number; +} + +export interface ContentProps { + $multiline?: boolean; +} diff --git a/src/components/common/JiraTicket/IconButton/index.tsx b/src/components/common/JiraTicket/IconButton/index.tsx new file mode 100644 index 000000000..1124f2f23 --- /dev/null +++ b/src/components/common/JiraTicket/IconButton/index.tsx @@ -0,0 +1,11 @@ +import { Tooltip } from "../../../common/Tooltip"; +import * as s from "./styles"; +import { IconButtonProps } from "./types"; + +export const IconButton = (props: IconButtonProps) => ( + + + + + +); diff --git a/src/components/common/JiraTicket/IconButton/styles.ts b/src/components/common/JiraTicket/IconButton/styles.ts new file mode 100644 index 000000000..faff4261e --- /dev/null +++ b/src/components/common/JiraTicket/IconButton/styles.ts @@ -0,0 +1,11 @@ +import styled from "styled-components"; + +export const Button = styled.button` + background: none; + border: none; + margin: 0; + padding: 4px; + cursor: ${({ disabled }) => (disabled ? "auto" : "pointer")}; + display: flex; + color: ${({ theme }) => theme.colors.field.icon}; +`; diff --git a/src/components/common/JiraTicket/IconButton/types.ts b/src/components/common/JiraTicket/IconButton/types.ts new file mode 100644 index 000000000..b3a6a8095 --- /dev/null +++ b/src/components/common/JiraTicket/IconButton/types.ts @@ -0,0 +1,9 @@ +import { ComponentType } from "react"; +import { IconProps } from "../../../common/icons/types"; + +export interface IconButtonProps { + icon: ComponentType; + onClick: () => void; + title: string; + disabled?: boolean; +} diff --git a/src/components/common/JiraTicket/JiraTicket.stories.tsx b/src/components/common/JiraTicket/JiraTicket.stories.tsx new file mode 100644 index 000000000..b35dc8b9e --- /dev/null +++ b/src/components/common/JiraTicket/JiraTicket.stories.tsx @@ -0,0 +1,24 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { JiraTicket } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "common/JiraTicket", + component: JiraTicket, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + summary: "Summary text", + description: { content: "Multiline\ndescription text", isLoading: false }, + attachment: { url: "https://www.example.com", fileName: "attachment.ext" } + } +}; diff --git a/src/components/common/JiraTicket/index.tsx b/src/components/common/JiraTicket/index.tsx new file mode 100644 index 000000000..116a0de0a --- /dev/null +++ b/src/components/common/JiraTicket/index.tsx @@ -0,0 +1,174 @@ +import copy from "copy-to-clipboard"; +import { useRef, useState } from "react"; +import { useTheme } from "styled-components"; +import { DefaultTheme } from "styled-components/dist/types"; +import { isString } from "../../../typeGuards/isString"; +import { addPrefix } from "../../../utils/addPrefix"; +import { downloadFile } from "../../../utils/downloadFile"; +import { sendTrackingEvent } from "../../../utils/sendTrackingEvent"; +import { CircleLoader } from "../../common/CircleLoader"; +import { CircleLoaderColors } from "../../common/CircleLoader/types"; +import { IconTag } from "../../common/IconTag"; +import { Tooltip } from "../../common/Tooltip"; +import { CopyIcon } from "../../common/icons/12px/CopyIcon"; +import { CrossIcon } from "../../common/icons/12px/CrossIcon"; +import { DownloadIcon } from "../../common/icons/12px/DownloadIcon"; +import { PaperclipIcon } from "../../common/icons/12px/PaperclipIcon"; +import { JiraLogoIcon } from "../../common/icons/16px/JiraLogoIcon"; +import { AttachmentTag } from "./AttachmentTag"; +import { Field } from "./Field"; +import { IconButton } from "./IconButton"; +import * as s from "./styles"; +import { trackingEvents } from "./tracking"; +import { JiraTicketProps } from "./types"; + +const getCircleLoaderColors = (theme: DefaultTheme): CircleLoaderColors => { + switch (theme.mode) { + case "light": + return { + start: "rgb(81 84 236 / 0%)", + end: "#5154ec", + background: "#fff" + }; + case "dark": + case "dark-jetbrains": + return { + start: "rgb(120 145 208 / 0%)", + end: "#7891d0", + background: "#222326" + }; + } +}; + +export const JiraTicket = (props: JiraTicketProps) => { + const [downloadErrorMessage, setDownloadErrorMessage] = useState(); + const descriptionContentRef = useRef(null); + const theme = useTheme(); + + const prefixedTrackingEvents = addPrefix( + props.tracking?.prefix || "", + trackingEvents + ); + + const handleCloseButtonClick = () => { + props.onClose(); + }; + + const copyToClipboard = ( + field: string, + value: HTMLElement | null | string + ) => { + sendTrackingEvent( + prefixedTrackingEvents.JIRA_TICKET_FIELD_COPY_BUTTON_CLICKED, + { + ...(props.tracking?.additionalInfo || {}), + field + } + ); + + if (value === null) { + return; + } + + if (isString(value)) { + copy(value); + } else { + copy(value.innerText); + } + }; + + const handleDownloadButtonClick = () => { + 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}` + ); + } + ); + } + }; + + return ( + + + + + Create Jira Ticket + Bug details + + + + + + + + copyToClipboard("summary", props.summary)} + /> + } + /> + + {props.description.isLoading ? ( + + + + ) : ( + props.description.content + )} + + } + errorMessage={props.description.errorMessage} + button={ + + copyToClipboard("description", descriptionContentRef.current) + } + /> + } + /> + {props.attachment && ( + + } + button={ + + } + errorMessage={downloadErrorMessage} + /> + )} + + ); +}; diff --git a/src/components/common/JiraTicket/styles.ts b/src/components/common/JiraTicket/styles.ts new file mode 100644 index 000000000..a6c84d9eb --- /dev/null +++ b/src/components/common/JiraTicket/styles.ts @@ -0,0 +1,49 @@ +import styled from "styled-components"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + border-radius: 7px; + border: 1px solid ${({ theme }) => theme.colors.jiraTicket.border}; + background: ${({ theme }) => theme.colors.jiraTicket.background}; + box-shadow: 0 1px 4px 0 rgb(0 0 0 / 45%); + padding: 12px; + gap: 12px; + font-size: 14px; + width: 100%; + box-sizing: border-box; +`; + +export const Header = styled.div` + display: flex; + gap: 12px; +`; + +export const TitleContainer = styled.div` + display: flex; + flex-direction: column; + color: ${({ theme }) => theme.colors.jiraTicket.text.secondary}; +`; + +export const Title = styled.span` + color: ${({ theme }) => theme.colors.jiraTicket.text.primary}; +`; + +export const CloseButton = styled.button` + background: none; + border: none; + margin: 0; + padding: 0; + display: flex; + cursor: pointer; + margin-left: auto; + height: fit-content; + color: ${({ theme }) => theme.colors.jiraTicket.icon}; +`; + +export const LoaderContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 200px; +`; diff --git a/src/components/common/JiraTicket/tracking.ts b/src/components/common/JiraTicket/tracking.ts new file mode 100644 index 000000000..b5399a538 --- /dev/null +++ b/src/components/common/JiraTicket/tracking.ts @@ -0,0 +1,6 @@ +export const trackingEvents = { + JIRA_TICKET_FIELD_COPY_BUTTON_CLICKED: + "jira ticket field copy button clicked", + JIRA_TICKET_ATTACHMENT_DOWNLOAD_BUTTON_CLICKED: + "jira ticket attachment download button clicked" +}; diff --git a/src/components/common/JiraTicket/types.ts b/src/components/common/JiraTicket/types.ts new file mode 100644 index 000000000..06796cba5 --- /dev/null +++ b/src/components/common/JiraTicket/types.ts @@ -0,0 +1,26 @@ +import { ReactNode } from "react"; + +export interface JiraTicketThemeColors { + background: string; + border: string; + text: { + primary: string; + secondary: string; + }; + icon: string; +} + +export interface JiraTicketProps { + summary: string; + description: { + content: ReactNode; + isLoading?: boolean; + errorMessage?: string; + }; + attachment?: { url: string; fileName: string }; + onClose: () => void; + tracking?: { + prefix?: string; + additionalInfo?: Record; + }; +} From 1e24e6379c7f2ad92038c7cc51f477735299cc6d Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Tue, 16 Jan 2024 18:00:24 +0100 Subject: [PATCH 10/13] Fix tracking event names --- src/components/common/JiraTicket/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/common/JiraTicket/index.tsx b/src/components/common/JiraTicket/index.tsx index 116a0de0a..f28ae3564 100644 --- a/src/components/common/JiraTicket/index.tsx +++ b/src/components/common/JiraTicket/index.tsx @@ -47,7 +47,8 @@ export const JiraTicket = (props: JiraTicketProps) => { const prefixedTrackingEvents = addPrefix( props.tracking?.prefix || "", - trackingEvents + trackingEvents, + " " ); const handleCloseButtonClick = () => { From abed2a90a52ad86ce3ee40879d0e1c78d20dd2ff Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Wed, 17 Jan 2024 11:00:22 +0100 Subject: [PATCH 11/13] Fix typos --- src/components/Tests/TestTicket/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Tests/TestTicket/index.tsx b/src/components/Tests/TestTicket/index.tsx index 3942da14d..5a0d4aff9 100644 --- a/src/components/Tests/TestTicket/index.tsx +++ b/src/components/Tests/TestTicket/index.tsx @@ -14,14 +14,14 @@ export const TestTicket = (props: TestTicketProps) => { .join("\n"); const description = [ - `${props.test.name}" test failed${ + `"${props.test.name}" test failed${ isString(props.test.errorOrFailMessage) ? ` with message: ${props.test.errorOrFailMessage}` : "" }`, `Last run at: ${new Date(props.test.runAt).toString()}`, `Duration: ${durationString}`, - relatedSpans.length > 0 ? `Related spans:\n"${relatedSpans}` : "" + relatedSpans.length > 0 ? `Related spans:\n${relatedSpans}` : "" ] .filter(Boolean) .join("\n\n"); From 66fa0b66dd0f4ebe35da9b6c4fb4b5630f8ed63e Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Mon, 22 Jan 2024 08:17:06 +0100 Subject: [PATCH 12/13] Fix formatting, rename feature flag --- .../HighNumberOfQueriesInsight/index.tsx | 17 +++++++------- .../HighNumberOfQueriesInsight/mockData.ts | 2 +- src/components/Insights/InsightCard/styles.ts | 18 +++++++-------- .../Insights/JiraTicket/Field/index.tsx | 2 +- src/components/Insights/JiraTicket/index.tsx | 2 +- .../EndpointNPlusOneInsightTicket/index.tsx | 23 ++++++++++--------- .../SpanBottleneckInsightTicket/index.tsx | 17 +++++++------- src/featureFlags.ts | 2 +- src/types.ts | 2 +- 9 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/components/Insights/HighNumberOfQueriesInsight/index.tsx b/src/components/Insights/HighNumberOfQueriesInsight/index.tsx index 28a6c5e91..5de559fc6 100644 --- a/src/components/Insights/HighNumberOfQueriesInsight/index.tsx +++ b/src/components/Insights/HighNumberOfQueriesInsight/index.tsx @@ -13,9 +13,9 @@ import { HighNumberOfQueriesInsightProps } from "./types"; export const HighNumberOfQueriesInsight = ( props: HighNumberOfQueriesInsightProps ) => { - const { insight } = props; + const { insight } = props; const traceId = insight.traceId; - + const handleTraceButtonClick = ( trace: Trace, insightType: InsightType, @@ -30,14 +30,15 @@ export const HighNumberOfQueriesInsight = ( content={ - {insight.quantile && insight.quantile === 0.95 && "Affecting the slowest 5% of requests. " } + {insight.quantile === 0.95 && + "Affecting the slowest 5% of requests. "} Consider using joins or caching responses to reduce database round trips # of Queries - + @@ -52,7 +53,7 @@ export const HighNumberOfQueriesInsight = ( - + {traceId && ( @@ -64,10 +65,10 @@ export const HighNumberOfQueriesInsight = ( handleTraceButtonClick( { id: traceId, - name: props.insight.spanInfo?.displayName + name: insight.spanInfo?.displayName }, - props.insight.type, - props.insight.spanInfo?.spanCodeObjectId + insight.type, + insight.spanInfo?.spanCodeObjectId ) } /> diff --git a/src/components/Insights/HighNumberOfQueriesInsight/mockData.ts b/src/components/Insights/HighNumberOfQueriesInsight/mockData.ts index 23c8e5d18..c30fe9e9a 100644 --- a/src/components/Insights/HighNumberOfQueriesInsight/mockData.ts +++ b/src/components/Insights/HighNumberOfQueriesInsight/mockData.ts @@ -69,5 +69,5 @@ export const mockedHighNumberOfQueriesInsight: EndpointHighNumberOfQueriesInsigh "method:org.springframework.samples.petclinic.owner.PetController$_$processCreationForm", customStartTime: null, actualStartTime: "2023-08-10T08:04:00Z", - quantile: 0.95, + quantile: 0.95 }; diff --git a/src/components/Insights/InsightCard/styles.ts b/src/components/Insights/InsightCard/styles.ts index f47c80939..79f6e3582 100644 --- a/src/components/Insights/InsightCard/styles.ts +++ b/src/components/Insights/InsightCard/styles.ts @@ -27,15 +27,15 @@ export const InsightIconContainer = styled.div` `; export const TicketIconContainer = styled.button` - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - padding: 2px; - position: relative; - background: none; - border: none; - cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 2px; + position: relative; + background: none; + border: none; + cursor: pointer; `; export const TicketIconLink = styled(Link)` diff --git a/src/components/Insights/JiraTicket/Field/index.tsx b/src/components/Insights/JiraTicket/Field/index.tsx index 60c68a25d..891200984 100644 --- a/src/components/Insights/JiraTicket/Field/index.tsx +++ b/src/components/Insights/JiraTicket/Field/index.tsx @@ -20,7 +20,7 @@ export const Field = (props: FieldProps) => { const scrollbarOffset = contentRef.current && - contentRef.current.scrollHeight > contentRef.current.clientHeight + contentRef.current.scrollHeight > contentRef.current.clientHeight ? scrollbar.width : 0; diff --git a/src/components/Insights/JiraTicket/index.tsx b/src/components/Insights/JiraTicket/index.tsx index a9b244f6d..66051dcb7 100644 --- a/src/components/Insights/JiraTicket/index.tsx +++ b/src/components/Insights/JiraTicket/index.tsx @@ -62,7 +62,7 @@ export const JiraTicket = (props: JiraTicketProps) => { const isLinkUnlinkInputVisible = getFeatureFlagValue( config, - FeatureFlag.IS_TICKET_LINK_UNLINK_INPUT_ENABLED + FeatureFlag.IS_INSIGHT_TICKET_LINKAGE_ENABLED ); const handleCloseButtonClick = () => { diff --git a/src/components/Insights/tickets/EndpointNPlusOneInsightTicket/index.tsx b/src/components/Insights/tickets/EndpointNPlusOneInsightTicket/index.tsx index 140980abb..08b8546b1 100644 --- a/src/components/Insights/tickets/EndpointNPlusOneInsightTicket/index.tsx +++ b/src/components/Insights/tickets/EndpointNPlusOneInsightTicket/index.tsx @@ -84,21 +84,22 @@ export const EndpointNPlusOneInsightTicket = ( ); const onReloadSpanInsight = () => { - spanInfo?.spanCodeObjectId && window.sendMessageToDigma({ - action: actions.GET_SPAN_INSIGHT, - payload: { - spanCodeObjectId: spanInfo?.spanCodeObjectId, - insightType: InsightType.SpanNPlusOne - } - }); - } + spanInfo?.spanCodeObjectId && + window.sendMessageToDigma({ + action: actions.GET_SPAN_INSIGHT, + payload: { + spanCodeObjectId: spanInfo?.spanCodeObjectId, + insightType: InsightType.SpanNPlusOne + } + }); + }; const traceId = span?.traceId; const attachment = traceId ? { - url: `${config.jaegerURL}/api/traces/${traceId}?prettyPrint=true`, - fileName: `trace-${traceId}.json` - } + url: `${config.jaegerURL}/api/traces/${traceId}?prettyPrint=true`, + fileName: `trace-${traceId}.json` + } : undefined; useEffect(() => { diff --git a/src/components/Insights/tickets/SpanBottleneckInsightTicket/index.tsx b/src/components/Insights/tickets/SpanBottleneckInsightTicket/index.tsx index 6c531893a..01452a646 100644 --- a/src/components/Insights/tickets/SpanBottleneckInsightTicket/index.tsx +++ b/src/components/Insights/tickets/SpanBottleneckInsightTicket/index.tsx @@ -75,14 +75,15 @@ export const SpanBottleneckInsightTicket = ( ); const onReloadSpanInsight = () => { - spanInsight?.spanInfo?.spanCodeObjectId && window.sendMessageToDigma({ - action: actions.GET_SPAN_INSIGHT, - payload: { - spanCodeObjectId: spanInsight?.spanInfo?.spanCodeObjectId, - insightType: InsightType.SpanEndpointBottleneck - } - }); - } + spanInsight?.spanInfo?.spanCodeObjectId && + window.sendMessageToDigma({ + action: actions.GET_SPAN_INSIGHT, + payload: { + spanCodeObjectId: spanInsight?.spanInfo?.spanCodeObjectId, + insightType: InsightType.SpanEndpointBottleneck + } + }); + }; useEffect(() => { const spanCodeObjectId = span?.spanInfo.spanCodeObjectId; diff --git a/src/featureFlags.ts b/src/featureFlags.ts index e1ed0fbfa..18a9532a9 100644 --- a/src/featureFlags.ts +++ b/src/featureFlags.ts @@ -7,7 +7,7 @@ const featureFlagMinBackendVersions: Record = { "v0.2.172-alpha.8", [FeatureFlag.IS_ASSETS_SERVICE_FILTER_VISIBLE]: "v0.2.174", [FeatureFlag.IS_ASSETS_OVERALL_IMPACT_HIDDEN]: "v0.2.181-alpha.1", - [FeatureFlag.IS_TICKET_LINK_UNLINK_INPUT_ENABLED]: "v0.2.200" + [FeatureFlag.IS_INSIGHT_TICKET_LINKAGE_ENABLED]: "v0.2.200" }; export const getFeatureFlagValue = ( diff --git a/src/types.ts b/src/types.ts index 5807c044e..6be3fb897 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,7 @@ export enum FeatureFlag { IS_DASHBOARD_CLIENT_SPANS_OVERALL_IMPACT_ENABLED, IS_ASSETS_SERVICE_FILTER_VISIBLE, IS_ASSETS_OVERALL_IMPACT_HIDDEN, - IS_TICKET_LINK_UNLINK_INPUT_ENABLED + IS_INSIGHT_TICKET_LINKAGE_ENABLED } export enum InsightType { From 59f19f726aa6f892bb0fad8793b52b23e743f4cf Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Mon, 22 Jan 2024 11:51:12 +0100 Subject: [PATCH 13/13] Create getDuration util, fix related spans in Test ticket --- .../Dashboard/widgets/SlowQueries/index.tsx | 3 ++- src/components/Insights/BottleneckInsight/index.tsx | 4 ++-- .../Insights/DurationBreakdownInsight/index.tsx | 11 +++++++---- src/components/Insights/DurationInsight/index.tsx | 4 +--- .../Insights/EndpointNPlusOneInsight/index.tsx | 5 ++--- src/components/Insights/NPlusOneInsight/index.tsx | 5 ++--- src/components/Insights/ScalingIssueInsight/index.tsx | 7 +++---- src/components/Insights/SlowEndpointInsight/index.tsx | 3 ++- .../Insights/SpanBottleneckInsight/index.tsx | 7 ++++--- .../tickets/common/BottleneckEndpoints/index.tsx | 4 ++-- .../LiveView/AreaTooltipContent/index.tsx | 5 ++--- .../LiveView/DotTooltipContent/index.tsx | 3 ++- .../RecentActivity/RecentActivityTable/index.tsx | 3 ++- src/components/Tests/TestCard/index.tsx | 3 ++- src/components/Tests/TestTicket/index.tsx | 6 +++--- src/components/Tests/index.tsx | 2 +- src/utils/getDurationString.ts | 4 ++++ 17 files changed, 43 insertions(+), 36 deletions(-) create mode 100644 src/utils/getDurationString.ts diff --git a/src/components/Dashboard/widgets/SlowQueries/index.tsx b/src/components/Dashboard/widgets/SlowQueries/index.tsx index 130a2143a..06968f609 100644 --- a/src/components/Dashboard/widgets/SlowQueries/index.tsx +++ b/src/components/Dashboard/widgets/SlowQueries/index.tsx @@ -1,3 +1,4 @@ +import { getDurationString } from "../../../../utils/getDurationString"; import { getPercentileKey } from "../../../../utils/getPercentileKey"; import { Tooltip } from "../../../common/Tooltip"; import { SnailIcon } from "../../../common/icons/SnailIcon"; @@ -15,7 +16,7 @@ const renderSlowQueryEntry = ( if (percentileViewMode) { const durationKey = getPercentileKey(percentileViewMode); const duration = durationKey ? item[durationKey] : undefined; - durationString = duration ? `${duration.value} ${duration.unit}` : ""; + durationString = duration ? getDurationString(duration) : ""; } const handleSpanClick = (spanCodeObjectId: string) => { diff --git a/src/components/Insights/BottleneckInsight/index.tsx b/src/components/Insights/BottleneckInsight/index.tsx index 05c89cd3c..85cbe602f 100644 --- a/src/components/Insights/BottleneckInsight/index.tsx +++ b/src/components/Insights/BottleneckInsight/index.tsx @@ -1,3 +1,4 @@ +import { getDurationString } from "../../../utils/getDurationString"; import { roundTo } from "../../../utils/roundTo"; import { sendTrackingEvent } from "../../../utils/sendTrackingEvent"; import { trimEndpointScheme } from "../../../utils/trimEndpointScheme"; @@ -48,8 +49,7 @@ export const BottleneckInsight = (props: BottleneckInsightProps) => { - {endpoint.avgDurationWhenBeingBottleneck.value}{" "} - {endpoint.avgDurationWhenBeingBottleneck.unit} + {getDurationString(endpoint.avgDurationWhenBeingBottleneck)} diff --git a/src/components/Insights/DurationBreakdownInsight/index.tsx b/src/components/Insights/DurationBreakdownInsight/index.tsx index 9228212d2..5ea672ebf 100644 --- a/src/components/Insights/DurationBreakdownInsight/index.tsx +++ b/src/components/Insights/DurationBreakdownInsight/index.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { usePagination } from "../../../hooks/usePagination"; +import { getDurationString } from "../../../utils/getDurationString"; import { getPercentileLabel } from "../../../utils/getPercentileLabel"; import { Pagination } from "../../common/Pagination"; import { Tooltip } from "../../common/Tooltip"; @@ -31,9 +32,9 @@ const getDurationTitle = (breakdownEntry: SpanDurationBreakdownEntry) => { let title = "Percentage of time spent in span:"; sortedPercentiles.forEach((percentile) => { - title += `\n${getPercentileLabel(percentile.percentile)}: ${ - percentile.duration.value - } ${percentile.duration.unit}`; + title += `\n${getPercentileLabel( + percentile.percentile + )}: ${getDurationString(percentile.duration)}`; }); return {title}; @@ -97,7 +98,9 @@ export const DurationBreakdownInsight = ( - {`${percentile.duration.value} ${percentile.duration.unit}`} + + {getDurationString(percentile.duration)} + ) : null; diff --git a/src/components/Insights/DurationInsight/index.tsx b/src/components/Insights/DurationInsight/index.tsx index fa6603e4e..31c3e3151 100644 --- a/src/components/Insights/DurationInsight/index.tsx +++ b/src/components/Insights/DurationInsight/index.tsx @@ -13,6 +13,7 @@ import { Duration } from "../../../globals"; import { isNumber } from "../../../typeGuards/isNumber"; import { convertToDuration } from "../../../utils/convertToDuration"; import { formatTimeDistance } from "../../../utils/formatTimeDistance"; +import { getDurationString } from "../../../utils/getDurationString"; import { getPercentileLabel } from "../../../utils/getPercentileLabel"; import { Button } from "../../common/Button"; import { Tooltip as CommonTooltip } from "../../common/Tooltip"; @@ -39,9 +40,6 @@ const MIN_CHART_CONTAINER_HEIGHT = 120; const CHART_Y_MARGIN = 20; const MIN_BAR_DISTANCE = 6; // minimum distance between the bars before moving the labels aside -const getDurationString = (duration: Duration) => - `${duration.value} ${duration.unit}`; - const getBarColor = (value: Duration, p50?: Duration, p95?: Duration) => { const blueColor = "#4b46a2"; const purpleColor = "#6f46a2"; diff --git a/src/components/Insights/EndpointNPlusOneInsight/index.tsx b/src/components/Insights/EndpointNPlusOneInsight/index.tsx index b74138f3f..2a1986391 100644 --- a/src/components/Insights/EndpointNPlusOneInsight/index.tsx +++ b/src/components/Insights/EndpointNPlusOneInsight/index.tsx @@ -2,6 +2,7 @@ import { useContext } from "react"; import { usePagination } from "../../../hooks/usePagination"; import { InsightType } from "../../../types"; import { getCriticalityLabel } from "../../../utils/getCriticalityLabel"; +import { getDurationString } from "../../../utils/getDurationString"; import { roundTo } from "../../../utils/roundTo"; import { sendTrackingEvent } from "../../../utils/sendTrackingEvent"; import { ConfigContext } from "../../common/App/ConfigContext"; @@ -103,9 +104,7 @@ export const EndpointNPlusOneInsight = ( Duration - - {span.duration.value} {span.duration.unit} - + {getDurationString(span.duration)} diff --git a/src/components/Insights/NPlusOneInsight/index.tsx b/src/components/Insights/NPlusOneInsight/index.tsx index ee8123cd7..57940d4c8 100644 --- a/src/components/Insights/NPlusOneInsight/index.tsx +++ b/src/components/Insights/NPlusOneInsight/index.tsx @@ -1,6 +1,7 @@ import { useContext } from "react"; import { InsightType } from "../../../types"; import { getCriticalityLabel } from "../../../utils/getCriticalityLabel"; +import { getDurationString } from "../../../utils/getDurationString"; import { sendTrackingEvent } from "../../../utils/sendTrackingEvent"; import { trimEndpointScheme } from "../../../utils/trimEndpointScheme"; import { ConfigContext } from "../../common/App/ConfigContext"; @@ -85,9 +86,7 @@ export const NPlusOneInsight = (props: NPlusOneInsightProps) => { Duration - - {props.insight.duration.value} {props.insight.duration.unit} - + {getDurationString(props.insight.duration)} Affected endpoints: diff --git a/src/components/Insights/ScalingIssueInsight/index.tsx b/src/components/Insights/ScalingIssueInsight/index.tsx index 4d022bfa3..51d22adfa 100644 --- a/src/components/Insights/ScalingIssueInsight/index.tsx +++ b/src/components/Insights/ScalingIssueInsight/index.tsx @@ -1,5 +1,6 @@ import { useContext } from "react"; import { InsightType } from "../../../types"; +import { getDurationString } from "../../../utils/getDurationString"; import { trimEndpointScheme } from "../../../utils/trimEndpointScheme"; import { ConfigContext } from "../../common/App/ConfigContext"; import { Button } from "../../common/Button"; @@ -52,10 +53,8 @@ export const ScalingIssueInsight = (props: ScalingIssueInsightProps) => { Duration - {props.insight.minDuration.value}{" "} - {props.insight.minDuration.unit} -{" "} - {props.insight.maxDuration.value}{" "} - {props.insight.maxDuration.unit} + {getDurationString(props.insight.minDuration)} -{" "} + {getDurationString(props.insight.maxDuration)} diff --git a/src/components/Insights/SlowEndpointInsight/index.tsx b/src/components/Insights/SlowEndpointInsight/index.tsx index 6fea6626e..5820c7962 100644 --- a/src/components/Insights/SlowEndpointInsight/index.tsx +++ b/src/components/Insights/SlowEndpointInsight/index.tsx @@ -1,3 +1,4 @@ +import { getDurationString } from "../../../utils/getDurationString"; import { roundTo } from "../../../utils/roundTo"; import { InsightCard } from "../InsightCard"; import { Description } from "../styles"; @@ -20,7 +21,7 @@ export const SlowEndpointInsight = (props: SlowEndpointInsightProps) => { )}%`} } - stats={`${props.insight.median.value} ${props.insight.median.unit}`} + stats={getDurationString(props.insight.median)} onRecalculate={props.onRecalculate} onRefresh={props.onRefresh} /> diff --git a/src/components/Insights/SpanBottleneckInsight/index.tsx b/src/components/Insights/SpanBottleneckInsight/index.tsx index 441661832..213d6f041 100644 --- a/src/components/Insights/SpanBottleneckInsight/index.tsx +++ b/src/components/Insights/SpanBottleneckInsight/index.tsx @@ -1,3 +1,4 @@ +import { getDurationString } from "../../../utils/getDurationString"; import { roundTo } from "../../../utils/roundTo"; import { sendTrackingEvent } from "../../../utils/sendTrackingEvent"; import { Tooltip } from "../../common/Tooltip"; @@ -50,9 +51,9 @@ export const SpanBottleneckInsight = (props: SpanBottleneckInsightProps) => { {`Slowing ${roundTo( span.probabilityOfBeingBottleneck * 100, 2 - )}% of the requests (~${ - span.avgDurationWhenBeingBottleneck.value - } ${span.avgDurationWhenBeingBottleneck.unit})`} + )}% of the requests (~${getDurationString( + span.avgDurationWhenBeingBottleneck + )})`} diff --git a/src/components/Insights/tickets/common/BottleneckEndpoints/index.tsx b/src/components/Insights/tickets/common/BottleneckEndpoints/index.tsx index b2e525e91..6b53fbb2e 100644 --- a/src/components/Insights/tickets/common/BottleneckEndpoints/index.tsx +++ b/src/components/Insights/tickets/common/BottleneckEndpoints/index.tsx @@ -1,3 +1,4 @@ +import { getDurationString } from "../../../../../utils/getDurationString"; import { roundTo } from "../../../../../utils/roundTo"; import { trimEndpointScheme } from "../../../../../utils/trimEndpointScheme"; import * as s from "./styles"; @@ -29,8 +30,7 @@ export const BottleneckEndpoints = (props: BottleneckEndpointsProps) => {
Slowing {roundTo(x.probabilityOfBeingBottleneck * 100, 2)}% of the - requests (~{x.avgDurationWhenBeingBottleneck.value}{" "} - {x.avgDurationWhenBeingBottleneck.unit}) + requests (~{getDurationString(x.avgDurationWhenBeingBottleneck)})
))} diff --git a/src/components/RecentActivity/LiveView/AreaTooltipContent/index.tsx b/src/components/RecentActivity/LiveView/AreaTooltipContent/index.tsx index 4115c2520..000e8ef7f 100644 --- a/src/components/RecentActivity/LiveView/AreaTooltipContent/index.tsx +++ b/src/components/RecentActivity/LiveView/AreaTooltipContent/index.tsx @@ -1,3 +1,4 @@ +import { getDurationString } from "../../../../utils/getDurationString"; import { TooltipContent } from "../TooltipContent"; import * as s from "./styles"; import { AreaTooltipContentProps } from "./types"; @@ -10,9 +11,7 @@ export const AreaTooltipContent = ( {[props.p95, props.p50].map((x) => ( {x.label} - - {x.duration.value} {x.duration.unit} - + {getDurationString(x.duration)} ))} diff --git a/src/components/RecentActivity/LiveView/DotTooltipContent/index.tsx b/src/components/RecentActivity/LiveView/DotTooltipContent/index.tsx index b60641eac..6fbaf0cd4 100644 --- a/src/components/RecentActivity/LiveView/DotTooltipContent/index.tsx +++ b/src/components/RecentActivity/LiveView/DotTooltipContent/index.tsx @@ -1,4 +1,5 @@ import { format } from "date-fns"; +import { getDurationString } from "../../../../utils/getDurationString"; import { TooltipContent } from "../TooltipContent"; import * as s from "./styles"; import { DotTooltipContentProps } from "./types"; @@ -16,7 +17,7 @@ export const DotTooltipContent = ( {format(date, "MM/dd/yyyy")} - {props.data.duration.value} {props.data.duration.unit} + {getDurationString(props.data.duration)} ); diff --git a/src/components/RecentActivity/RecentActivityTable/index.tsx b/src/components/RecentActivity/RecentActivityTable/index.tsx index 5d4e6afc1..18adaf07f 100644 --- a/src/components/RecentActivity/RecentActivityTable/index.tsx +++ b/src/components/RecentActivity/RecentActivityTable/index.tsx @@ -8,6 +8,7 @@ import { useMemo } from "react"; import { Duration } from "../../../globals"; import { isNumber } from "../../../typeGuards/isNumber"; import { formatTimeDistance } from "../../../utils/formatTimeDistance"; +import { getDurationString } from "../../../utils/getDurationString"; import { getInsightTypeInfo } from "../../../utils/getInsightTypeInfo"; import { getInsightTypeOrderPriority } from "../../../utils/getInsightTypeOrderPriority"; import { greenScale } from "../../common/App/getTheme"; @@ -62,7 +63,7 @@ const renderTimeDistance = (timestamp: string, viewMode: ViewMode) => { const renderDuration = (duration: Duration, viewMode: ViewMode) => viewMode === "table" ? ( - + ) : ( {duration.value} diff --git a/src/components/Tests/TestCard/index.tsx b/src/components/Tests/TestCard/index.tsx index 7771d4b19..012348e81 100644 --- a/src/components/Tests/TestCard/index.tsx +++ b/src/components/Tests/TestCard/index.tsx @@ -1,5 +1,6 @@ import { isString } from "../../../typeGuards/isString"; import { formatTimeDistance } from "../../../utils/formatTimeDistance"; +import { getDurationString } from "../../../utils/getDurationString"; import { sendTrackingEvent } from "../../../utils/sendTrackingEvent"; import { NewButton } from "../../common/NewButton"; import { Tag } from "../../common/Tag"; @@ -102,7 +103,7 @@ export const TestCard = (props: TestCardProps) => { }); }; - const durationString = `${props.test.duration.value} ${props.test.duration.unit}`; + const durationString = getDurationString(props.test.duration); return ( diff --git a/src/components/Tests/TestTicket/index.tsx b/src/components/Tests/TestTicket/index.tsx index 5a0d4aff9..2f78ce92b 100644 --- a/src/components/Tests/TestTicket/index.tsx +++ b/src/components/Tests/TestTicket/index.tsx @@ -1,11 +1,11 @@ import { isString } from "../../../typeGuards/isString"; +import { getDurationString } from "../../../utils/getDurationString"; import { JiraTicket } from "../../common/JiraTicket"; import { TestTicketProps } from "./types"; export const TestTicket = (props: TestTicketProps) => { const summary = `"${props.test.name}" test failed`; - const durationString = `${props.test.duration.value} ${props.test.duration.unit}`; const relatedSpans = props.spanContexts .filter((x) => props.test.contextsSpanCodeObjectIds.includes(x.spanCodeObjectId) @@ -16,11 +16,11 @@ export const TestTicket = (props: TestTicketProps) => { const description = [ `"${props.test.name}" test failed${ isString(props.test.errorOrFailMessage) - ? ` with message: ${props.test.errorOrFailMessage}` + ? ` with message:\n${props.test.errorOrFailMessage}` : "" }`, `Last run at: ${new Date(props.test.runAt).toString()}`, - `Duration: ${durationString}`, + `Duration: ${getDurationString(props.test.duration)}`, relatedSpans.length > 0 ? `Related spans:\n${relatedSpans}` : "" ] .filter(Boolean) diff --git a/src/components/Tests/index.tsx b/src/components/Tests/index.tsx index e7701bc5f..36d865a70 100644 --- a/src/components/Tests/index.tsx +++ b/src/components/Tests/index.tsx @@ -274,7 +274,7 @@ export const Tests = (props: TestsProps) => { {config.userRegistrationEmail ? ( ) : ( diff --git a/src/utils/getDurationString.ts b/src/utils/getDurationString.ts new file mode 100644 index 000000000..fafb731d0 --- /dev/null +++ b/src/utils/getDurationString.ts @@ -0,0 +1,4 @@ +import { Duration } from "../globals"; + +export const getDurationString = (duration: Duration) => + `${duration.value} ${duration.unit}`;