diff --git a/.storybook/preview-body.html b/.storybook/preview-body.html index 27c330858..e20c1099f 100644 --- a/.storybook/preview-body.html +++ b/.storybook/preview-body.html @@ -35,6 +35,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-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 223782271..06e34c7ed 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", @@ -101,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", diff --git a/src/actions.ts b/src/actions.ts index 105f2f5cb..306850000 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -26,5 +26,7 @@ export const actions = addPrefix(ACTION_PREFIX, { GET_BACKEND_INFO: "GET_BACKEND_INFO", SET_BACKEND_INFO: "SET_BACKEND_INFO", REGISTER: "REGISTER", + SET_ENVIRONMENTS: "SET_ENVIRONMENTS", + SET_SELECTED_CODE_SCOPE: "SET_SELECTED_CODE_SCOPE", SET_IS_MICROMETER_PROJECT: "SET_IS_MICROMETER_PROJECT" }); 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/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/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/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/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/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..012348e81 --- /dev/null +++ b/src/components/Tests/TestCard/index.tsx @@ -0,0 +1,168 @@ +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"; +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 { trackingEvents } from "../tracking"; +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 = () => { + sendTrackingEvent(trackingEvents.TEST_NAME_LINK_CLICKED); + 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 = () => { + 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 + ); + + return context.spanCodeObjectId === id; + }); + + window.sendMessageToDigma({ + action: actions.GO_TO_TRACE, + payload: { + traceId: props.test.traceId, + displayName: spanContext?.displayName, + spanCodeObjectId: spanContext?.spanCodeObjectId + } + }); + }; + + const handleRunButtonClick = () => { + sendTrackingEvent(trackingEvents.RUN_TEST_BUTTON_CLICKED); + window.sendMessageToDigma({ + action: actions.RUN_TEST, + payload: { + methodCodeObjectId: props.test.spanInfo.methodCodeObjectId + } + }); + }; + + const durationString = getDurationString(props.test.duration); + + 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/TestTicket/index.tsx b/src/components/Tests/TestTicket/index.tsx new file mode 100644 index 000000000..2f78ce92b --- /dev/null +++ b/src/components/Tests/TestTicket/index.tsx @@ -0,0 +1,39 @@ +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 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:\n${props.test.errorOrFailMessage}` + : "" + }`, + `Last run at: ${new Date(props.test.runAt).toString()}`, + `Duration: ${getDurationString(props.test.duration)}`, + 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 new file mode 100644 index 000000000..9764db579 --- /dev/null +++ b/src/components/Tests/Tests.stories.tsx @@ -0,0 +1,93 @@ +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 = { + 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 = { + args: { + data: { + data: { + paging: { + pageNumber: 1, + pageSize: 10, + totalCount: 12 + }, + spanContexts: [ + { + displayName: "spanDisplayName", + spanCodeObjectId: "123", + methodCodeObjectId: "methodCodeObjectId123" + } + ], + entries: [ + { ...mockedTest, name: "Test 1", result: "fail" }, + { ...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 new file mode 100644 index 000000000..36d865a70 --- /dev/null +++ b/src/components/Tests/index.tsx @@ -0,0 +1,292 @@ +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 { sendTrackingEvent } from "../../utils/sendTrackingEvent"; +import { MenuItem } from "../Assets/FilterMenu/types"; +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 { 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; +const REFRESH_INTERVAL = isNumber(window.testsRefreshInterval) + ? window.testsRefreshInterval + : 10 * 1000; // in milliseconds + +export const Tests = (props: TestsProps) => { + const [data, setData] = useState(); + const previousData = usePrevious(data); + const [page, setPage] = useState(0); + 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, + payload: { + pageSize: PAGE_SIZE + } + }); + + sendTrackingEvent(trackingEvents.PAGE_LOADED); + + window.sendMessageToDigma({ + action: actions.GET_SPAN_LATEST_DATA, + payload: payloadToSend + }); + setIsInitialLoading(true); + + 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); + }; + }, []); + + useEffect(() => { + if (previousLastSetDataTimeStamp !== lastSetDataTimeStamp) { + window.clearTimeout(refreshTimerId.current); + refreshTimerId.current = window.setTimeout(() => { + window.sendMessageToDigma({ + action: actions.GET_SPAN_LATEST_DATA, + payload: payloadToSend + }); + }, REFRESH_INTERVAL); + } + }, [previousLastSetDataTimeStamp, lastSetDataTimeStamp, payloadToSend]); + + useEffect(() => { + if (previousPayloadToSend && previousPayloadToSend !== payloadToSend) { + window.sendMessageToDigma({ + action: actions.GET_SPAN_LATEST_DATA, + payload: payloadToSend + }); + } + }, [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) { + setIsInitialLoading(false); + } + }, [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 ( + + + + ); + } + + 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 ( + + + Environment + + + {renderContent()} + {testToOpenTicketPopup && ( + + + {config.userRegistrationEmail ? ( + + ) : ( + + )} + + + )} + + ); +}; diff --git a/src/components/Tests/styles.ts b/src/components/Tests/styles.ts new file mode 100644 index 000000000..01b5bd974 --- /dev/null +++ b/src/components/Tests/styles.ts @@ -0,0 +1,94 @@ +import styled from "styled-components"; +import { LAYERS } from "../common/App/styles"; + +export const Container = styled.div` + background: ${({ theme }) => theme.colors.panel.background}; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +`; + +export const NoDataContainer = styled.div` + flex-grow: 1; + 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/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/Tests/types.ts b/src/components/Tests/types.ts new file mode 100644 index 000000000..fd68da6cf --- /dev/null +++ b/src/components/Tests/types.ts @@ -0,0 +1,42 @@ +import { Duration } from "../../globals"; +import { SpanInfo } from "../../types"; + +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; + }; + 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 4d00b4305..b785d50d8 100644 --- a/src/components/common/App/ConfigContext.ts +++ b/src/components/common/App/ConfigContext.ts @@ -18,5 +18,7 @@ export const ConfigContext = createContext({ : "", environment: isString(window.environment) ? window.environment : "", backendInfo: undefined, + environments: undefined, + scope: undefined, isMicrometerProject: window.isMicrometerProject === true }); diff --git a/src/components/common/App/getTheme.ts b/src/components/common/App/getTheme.ts index 07a990b54..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: { @@ -234,11 +239,36 @@ const darkThemeColors: ThemeColors = { border: grayScale[700], icon: grayScale[200], text: grayScale[100] + }, + 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: { @@ -415,6 +445,25 @@ const lightThemeColors: ThemeColors = { border: grayScale[300], icon: grayScale[800], text: grayScale[800] + }, + 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] } }; diff --git a/src/components/common/App/index.tsx b/src/components/common/App/index.tsx index c3eb1690d..aefadff97 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 + })); + } + }; + const handleSetIsMicrometerProject = (data: unknown) => { if (isObject(data) && isBoolean(data.isMicrometerProject)) { setConfig((config) => ({ @@ -235,6 +259,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 + ); dispatcher.addActionListener( actions.SET_IS_MICROMETER_PROJECT, handleSetIsMicrometerProject @@ -292,6 +324,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 + ); dispatcher.removeActionListener( actions.SET_IS_MICROMETER_PROJECT, handleSetIsMicrometerProject diff --git a/src/components/common/App/types.ts b/src/components/common/App/types.ts index b7e6725d2..057f468a3 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,5 +51,7 @@ export interface ConfigContextData { userRegistrationEmail: string; environment: string; backendInfo: BackendInfo | undefined; + environments: Environment[] | undefined; + scope: Scope | undefined; isMicrometerProject: boolean; } 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..f28ae3564 --- /dev/null +++ b/src/components/common/JiraTicket/index.tsx @@ -0,0 +1,175 @@ +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; + }; +} diff --git a/src/components/common/NewButton/index.tsx b/src/components/common/NewButton/index.tsx index bca665bd6..f107fca7f 100644 --- a/src/components/common/NewButton/index.tsx +++ b/src/components/common/NewButton/index.tsx @@ -1,13 +1,18 @@ +import { ForwardedRef, forwardRef } from "react"; import * as s from "./styles"; import { NewButtonProps } from "./types"; -export const NewButton = (props: NewButtonProps) => { +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); 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/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/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/globals.d.ts b/src/globals.d.ts index a150b95cb..306251f02 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -51,6 +51,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..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; @@ -31,6 +35,27 @@ export interface ThemeColors { attachmentTag: AttachmentTagThemeColors; jiraTicket: JiraTicketThemeColors; field: FieldThemeColors; + 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" { 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 { 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}`; diff --git a/webpackEntries.ts b/webpackEntries.ts index d99d5c055..7c9457cc7 100644 --- a/webpackEntries.ts +++ b/webpackEntries.ts @@ -43,6 +43,10 @@ export const entries: AppEntries = { "recentActivityIsEnvironmentManagementEnabled" ] }, + tests: { + entry: path.resolve(__dirname, "./src/containers/Tests/index.tsx"), + environmentVariables: ["testsRefreshInterval"] + }, troubleshooting: { entry: path.resolve(__dirname, "./src/containers/Troubleshooting/index.tsx") }