diff --git a/src/components/Highlights/Highlights.stories.tsx b/src/components/Highlights/Highlights.stories.tsx index 84fc6cd74..020b5b1cf 100644 --- a/src/components/Highlights/Highlights.stories.tsx +++ b/src/components/Highlights/Highlights.stories.tsx @@ -8,20 +8,11 @@ import { ConfigContext, initialState } from "../common/App/ConfigContext"; import { DeploymentType } from "../common/App/types"; import { mockedImpactData } from "./Impact/mockData"; import { mockedPerformanceData } from "./Performance/mockData"; +import { mockedScalingData } from "./Scaling/mockData"; import { mockedTopIssuesData } from "./TopIssues/mockData"; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction -const mockedConfig = { - ...initialState, - backendInfo: { - applicationVersion: - featureFlagMinBackendVersions[FeatureFlag.ARE_IMPACT_HIGHLIGHTS_ENABLED], - deploymentType: DeploymentType.HELM, - centralize: true - } -}; - const meta: Meta = { title: "Highlights/Highlights", component: Highlights, @@ -42,8 +33,25 @@ export default meta; type Story = StoryObj; +const mockedConfig = { + ...initialState, + backendInfo: { + applicationVersion: + featureFlagMinBackendVersions[FeatureFlag.ARE_SCALING_HIGHLIGHTS_ENABLED], + deploymentType: DeploymentType.HELM, + centralize: true + } +}; + // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args export const Default: Story = { + decorators: [ + (Story) => ( + + + + ) + ], play: () => { window.setTimeout(() => { window.postMessage({ @@ -61,11 +69,23 @@ export const Default: Story = { action: mainActions.SET_HIGHLIGHTS_IMPACT_DATA, payload: mockedImpactData }); + window.postMessage({ + type: "digma", + action: mainActions.SET_HIGHLIGHTS_SCALING_DATA, + payload: mockedScalingData + }); }, 1000); } }; export const Empty: Story = { + decorators: [ + (Story) => ( + + + + ) + ], play: () => { window.setTimeout(() => { window.postMessage({ @@ -83,6 +103,11 @@ export const Empty: Story = { action: mainActions.SET_HIGHLIGHTS_IMPACT_DATA, payload: { impactHighlights: [] } }); + window.postMessage({ + type: "digma", + action: mainActions.SET_HIGHLIGHTS_SCALING_DATA, + payload: { scaling: [] } + }); }, 1000); } }; diff --git a/src/components/Highlights/Impact/Impact.stories.tsx b/src/components/Highlights/Impact/Impact.stories.tsx index 8c3bf55ba..f4bb5cdc9 100644 --- a/src/components/Highlights/Impact/Impact.stories.tsx +++ b/src/components/Highlights/Impact/Impact.stories.tsx @@ -82,7 +82,7 @@ export const Empty: Story = { } }; -export const Disabled: Story = { +export const Locked: Story = { decorators: [ (Story) => ( = { + title: "Highlights/Scaling", + component: Scaling, + 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 + +const mockedConfig = { + ...initialState, + backendInfo: { + applicationVersion: + featureFlagMinBackendVersions[FeatureFlag.ARE_IMPACT_HIGHLIGHTS_ENABLED], + deploymentType: DeploymentType.HELM, + centralize: true + } +}; + +export const NoData: Story = { + decorators: [ + (Story) => ( + + + + ) + ], + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_HIGHLIGHTS_SCALING_DATA, + payload: { ...mockedScalingData, dataState: "noData" } + }); + }); + } +}; + +export const PartialData: Story = { + decorators: [ + (Story) => ( + + + + ) + ], + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_HIGHLIGHTS_SCALING_DATA, + payload: { ...mockedScalingData, dataState: "partial" } + }); + }); + } +}; + +export const ScalingWell: Story = { + decorators: [ + (Story) => ( + + + + ) + ], + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_HIGHLIGHTS_SCALING_DATA, + payload: { ...mockedScalingData, dataState: "scalingWell" } + }); + }); + } +}; + +export const ScalingBadly: Story = { + decorators: [ + (Story) => ( + + + + ) + ], + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_HIGHLIGHTS_SCALING_DATA, + payload: mockedScalingData + }); + }); + } +}; diff --git a/src/components/Highlights/Scaling/index.tsx b/src/components/Highlights/Scaling/index.tsx new file mode 100644 index 000000000..edd7ceced --- /dev/null +++ b/src/components/Highlights/Scaling/index.tsx @@ -0,0 +1,169 @@ +import { Row, createColumnHelper } from "@tanstack/react-table"; +import { useContext, useEffect } from "react"; +import { actions as globalActions } from "../../../actions"; +import { ROUTES, SCALING_ISSUE_DOCUMENTATION_URL } from "../../../constants"; +import { ChangeViewPayload } from "../../../types"; +import { openURLInDefaultBrowser } from "../../../utils/actions/openURLInDefaultBrowser"; +import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; +import { getDurationString } from "../../../utils/getDurationString"; +import { ConfigContext } from "../../common/App/ConfigContext"; +import { CrossCircleIcon } from "../../common/icons/16px/CrossCircleIcon"; +import { RefreshIcon } from "../../common/icons/16px/RefreshIcon"; +import { CheckCircleIcon } from "../../common/icons/20px/CheckCircleIcon"; +import { Button } from "../../common/v3/Button"; +import { Card } from "../../common/v3/Card"; +import { EmptyStateCard } from "../EmptyStateCard"; +import { addEnvironmentColumns } from "../TopIssues/highlightCards/addEnvironmentColumns"; +import { EnvironmentData } from "../TopIssues/types"; +import { Section } from "../common/Section"; +import { Table } from "../common/Table"; +import { TableText } from "../common/TableText"; +import { handleEnvironmentTableRowClick } from "../handleEnvironmentTableRowClick"; +import { trackingEvents } from "../tracking"; +import * as s from "./styles"; +import { EnvironmentScalingData } from "./types"; +import { useScalingData } from "./useScalingData"; + +export const Scaling = () => { + const { data, getData } = useScalingData(); + const config = useContext(ConfigContext); + + useEffect(() => { + getData(); + }, []); + + const renderScalingCard = ( + data: EnvironmentData[] + ) => { + const columnHelper = + createColumnHelper>(); + + const metricsColumns = [ + columnHelper.accessor((x) => x.metrics.concurrency, { + header: "Concurrency", + cell: (info) => { + const value = info.getValue(); + const concurrencyString = String(value); + + return ( + {concurrencyString} + ); + } + }), + columnHelper.accessor((x) => x.metrics.duration, { + header: "Duration", + cell: (info) => { + const value = info.getValue(); + const durationString = getDurationString(value); + + return {durationString}; + } + }) + ]; + + const columns = addEnvironmentColumns(columnHelper, metricsColumns); + + const handleTableRowClick = ( + row: Row> + ) => { + sendUserActionTrackingEvent( + trackingEvents.SCALING_CARD_TABLE_ROW_CLICKED + ); + handleEnvironmentTableRowClick( + config.environments, + row.original.environmentId, + ROUTES.INSIGHTS + ); + }; + + return ( + Scaling badly} + content={ + > + columns={columns} + data={data} + onRowClick={handleTableRowClick} + /> + } + /> + ); + }; + + const renderCard = () => { + const handleLearnMoreButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.SCALING_CARD_LEARN_MORE_BUTTON_CLICKED + ); + + openURLInDefaultBrowser(SCALING_ISSUE_DOCUMENTATION_URL); + }; + + const handleViewAnalyticsButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.SCALING_CARD_VIEW_ANALYTICS_BUTTON_CLICKED + ); + + window.sendMessageToDigma({ + action: globalActions.CHANGE_VIEW, + payload: { + view: ROUTES.ANALYTICS + } + }); + }; + + if (!data) { + return null; + } + + if (data?.dataState === "NoData") { + return ( + + } + /> + ); + } + + if (data?.dataState === "Partial") { + return ( + + ); + } + + if (data?.dataState === "ScalingWell") { + return ( + + } + /> + ); + } + + return renderScalingCard(data.scaling); + }; + + return
{renderCard()}
; +}; diff --git a/src/components/Highlights/Scaling/mockData.ts b/src/components/Highlights/Scaling/mockData.ts new file mode 100644 index 000000000..1830cae9e --- /dev/null +++ b/src/components/Highlights/Scaling/mockData.ts @@ -0,0 +1,50 @@ +import { InsightStatus } from "../../Insights/types"; +import { ScalingData } from "./types"; + +export const mockedScalingData: ScalingData = { + dataState: "ScalingBadly", + scaling: [ + { + environmentId: "1", + environmentName: "Production", + insightStatus: InsightStatus.Active, + criticality: 0.8, + metrics: { + concurrency: 100, + duration: { + value: 22.71, + unit: "ms", + raw: 22705900.0 + } + } + }, + { + environmentId: "2", + environmentName: "Staging", + insightStatus: InsightStatus.Active, + criticality: 0.8, + metrics: { + concurrency: 50, + duration: { + value: 22.71, + unit: "ms", + raw: 22705900.0 + } + } + }, + { + environmentId: "3", + environmentName: "Development", + insightStatus: InsightStatus.Active, + criticality: 0.8, + metrics: { + concurrency: 20, + duration: { + value: 22.71, + unit: "ms", + raw: 22705900.0 + } + } + } + ] +}; diff --git a/src/components/Highlights/Scaling/styles.ts b/src/components/Highlights/Scaling/styles.ts new file mode 100644 index 000000000..9a99e0c71 --- /dev/null +++ b/src/components/Highlights/Scaling/styles.ts @@ -0,0 +1,6 @@ +import styled from "styled-components"; +import { bodySemiboldTypography } from "../../common/App/typographies"; + +export const CardTitle = styled.div` + ${bodySemiboldTypography} +`; diff --git a/src/components/Highlights/Scaling/types.ts b/src/components/Highlights/Scaling/types.ts new file mode 100644 index 000000000..7a84825ab --- /dev/null +++ b/src/components/Highlights/Scaling/types.ts @@ -0,0 +1,19 @@ +import { Duration } from "../../../globals"; +import { EnvironmentData } from "../TopIssues/types"; + +export type EnvironmentScalingData = { + concurrency: number; + duration: Duration; +}; + +export type ScalingData = { + dataState: "NoData" | "Partial" | "ScalingWell" | "ScalingBadly"; + scaling: EnvironmentData[]; +}; + +export interface GetHighlightsScalingDataPayload { + query: { + scopedSpanCodeObjectId: string | null; + environments: string[]; + }; +} diff --git a/src/components/Highlights/Scaling/useScalingData.ts b/src/components/Highlights/Scaling/useScalingData.ts new file mode 100644 index 000000000..3740e95e7 --- /dev/null +++ b/src/components/Highlights/Scaling/useScalingData.ts @@ -0,0 +1,72 @@ +import { useCallback, useContext, useEffect, useRef, useState } from "react"; +import { dispatcher } from "../../../dispatcher"; +import { usePrevious } from "../../../hooks/usePrevious"; +import { actions as mainActions } from "../../Main/actions"; +import { ConfigContext } from "../../common/App/ConfigContext"; +import { GetHighlightsScalingDataPayload, ScalingData } from "./types"; + +const REFRESH_INTERVAL = 10 * 1000; // in milliseconds + +export const useScalingData = () => { + const [data, setData] = useState(); + const config = useContext(ConfigContext); + const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); + const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); + const refreshTimerId = useRef(); + + const getData = useCallback(() => { + window.sendMessageToDigma({ + action: mainActions.GET_HIGHLIGHTS_SCALING_DATA, + payload: { + query: { + scopedSpanCodeObjectId: config.scope?.span?.spanCodeObjectId || null, + environments: config.environments?.map((x) => x.id) || [] + } + } + }); + }, [config.scope?.span?.spanCodeObjectId, config.environments]); + const previousGetData = usePrevious(getData); + + useEffect(() => { + if (previousGetData && previousGetData !== getData) { + window.clearTimeout(refreshTimerId.current); + + getData(); + } + }, [previousGetData, getData]); + + useEffect(() => { + if ( + previousLastSetDataTimeStamp && + previousLastSetDataTimeStamp !== lastSetDataTimeStamp + ) { + refreshTimerId.current = window.setTimeout(() => { + getData(); + }, REFRESH_INTERVAL); + } + }, [previousLastSetDataTimeStamp, lastSetDataTimeStamp, getData]); + + useEffect(() => { + const handleScalingData = (data: any, timeStamp: number) => { + setData(data as ScalingData); + setLastSetDataTimeStamp(timeStamp); + }; + + dispatcher.addActionListener( + mainActions.SET_HIGHLIGHTS_SCALING_DATA, + handleScalingData + ); + + return () => { + dispatcher.removeActionListener( + mainActions.SET_HIGHLIGHTS_SCALING_DATA, + handleScalingData + ); + }; + }, []); + + return { + data, + getData + }; +}; diff --git a/src/components/Highlights/index.tsx b/src/components/Highlights/index.tsx index c8a57f36a..b08838c6a 100644 --- a/src/components/Highlights/index.tsx +++ b/src/components/Highlights/index.tsx @@ -4,6 +4,7 @@ import { FeatureFlag } from "../../types"; import { ConfigContext } from "../common/App/ConfigContext"; import { Impact } from "./Impact"; import { Performance } from "./Performance"; +import { Scaling } from "./Scaling"; import { TopIssues } from "./TopIssues"; import * as s from "./styles"; @@ -13,12 +14,17 @@ export const Highlights = () => { config, FeatureFlag.ARE_IMPACT_HIGHLIGHTS_ENABLED ); + const areScalingHighlightsVisible = getFeatureFlagValue( + config, + FeatureFlag.ARE_SCALING_HIGHLIGHTS_ENABLED + ); return ( {areImpactHighlightsVisible && } + {areScalingHighlightsVisible && } ); }; diff --git a/src/components/Highlights/tracking.ts b/src/components/Highlights/tracking.ts index e35cf5d7e..25f02971a 100644 --- a/src/components/Highlights/tracking.ts +++ b/src/components/Highlights/tracking.ts @@ -11,7 +11,12 @@ export const trackingEvents = addPrefix( PERFORMANCE_CARD_TABLE_ROW_CLICKED: "performance card table row clicked", IMPACT_CARD_TABLE_ROW_CLICKED: "impact card table row clicked", IMPACT_CARD_LEARN_MORE_BUTTON_CLICKED: - "impact card learn more button clicked" + "impact card learn more button clicked", + SCALING_CARD_TABLE_ROW_CLICKED: "scaling card table row clicked", + SCALING_CARD_LEARN_MORE_BUTTON_CLICKED: + "scaling card learn more button clicked", + SCALING_CARD_VIEW_ANALYTICS_BUTTON_CLICKED: + "scaling card view analytics button clicked" }, " " ); diff --git a/src/components/Main/actions.ts b/src/components/Main/actions.ts index 6cf412d8e..b31149c6c 100644 --- a/src/components/Main/actions.ts +++ b/src/components/Main/actions.ts @@ -10,5 +10,7 @@ export const actions = addPrefix(ACTION_PREFIX, { GET_HIGHLIGHTS_PERFORMANCE_DATA: "GET_HIGHLIGHTS_PERFORMANCE_DATA", SET_HIGHLIGHTS_PERFORMANCE_DATA: "SET_HIGHLIGHTS_PERFORMANCE_DATA", GET_HIGHLIGHTS_IMPACT_DATA: "GET_HIGHLIGHTS_IMPACT_DATA", - SET_HIGHLIGHTS_IMPACT_DATA: "SET_HIGHLIGHTS_IMPACT_DATA" + SET_HIGHLIGHTS_IMPACT_DATA: "SET_HIGHLIGHTS_IMPACT_DATA", + GET_HIGHLIGHTS_SCALING_DATA: "GET_HIGHLIGHTS_SCALING_DATA", + SET_HIGHLIGHTS_SCALING_DATA: "SET_HIGHLIGHTS_SCALING_DATA" }); diff --git a/src/constants.ts b/src/constants.ts index 5155f16e7..527b3b21e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -21,6 +21,12 @@ export const INSTRUMENTATION_DOCUMENTATION_URL = export const PERFORMANCE_IMPACT_DOCUMENTATION_URL = "https://docs.digma.ai/digma-developer-guide/use-cases-wip/prioritize-technical-debt#asset-performance-impact"; +export const SCALING_ISSUE_DOCUMENTATION_URL = + "https://docs.digma.ai/digma-developer-guide/digma-features/insights/scaling-issue"; + +export const TEST_OBSERVABILITY_DOCUMENTATION_URL = + "https://docs.digma.ai/digma-developer-guide/digma-features/test-observability"; + export const PERCENTILES: { label: string; percentile: number; diff --git a/src/featureFlags.ts b/src/featureFlags.ts index 35408031a..acb90e2e9 100644 --- a/src/featureFlags.ts +++ b/src/featureFlags.ts @@ -4,6 +4,7 @@ import { FeatureFlag } from "./types"; export const featureFlagMinBackendVersions: Record = { [FeatureFlag.ARE_IMPACT_HIGHLIGHTS_ENABLED]: "0.3.7", + [FeatureFlag.ARE_SCALING_HIGHLIGHTS_ENABLED]: "0.3.13", [FeatureFlag.ARE_INSIGHT_STATS_SUPPORTED]: "0.3.7" }; diff --git a/src/types.ts b/src/types.ts index 5be2ad208..74d2771f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ import { Duration } from "./globals"; export enum FeatureFlag { ARE_IMPACT_HIGHLIGHTS_ENABLED, + ARE_SCALING_HIGHLIGHTS_ENABLED, ARE_INSIGHT_STATS_SUPPORTED }