diff --git a/src/components/Assets/AssetList/index.tsx b/src/components/Assets/AssetList/index.tsx index 5719ca027..140cc4367 100644 --- a/src/components/Assets/AssetList/index.tsx +++ b/src/components/Assets/AssetList/index.tsx @@ -13,7 +13,6 @@ import { isEnvironment } from "../../../typeGuards/isEnvironment"; import { isNumber } from "../../../typeGuards/isNumber"; import { isString } from "../../../typeGuards/isString"; import { ConfigContext } from "../../common/App/ConfigContext"; -import { DeploymentType } from "../../common/App/types"; import { EmptyState } from "../../common/EmptyState"; import { Menu } from "../../common/Menu"; import { NewCircleLoader } from "../../common/NewCircleLoader"; @@ -227,8 +226,7 @@ export const AssetList = (props: AssetListProps) => { const isImpactHidden = useMemo( () => !( - config.backendInfo?.deploymentType === DeploymentType.HELM && - config.environment?.type === "Public" + config.backendInfo?.centralize && config.environment?.type === "Public" ), [config.backendInfo?.deploymentType, config.environment?.type] ); diff --git a/src/components/Highlights/Highlights.stories.tsx b/src/components/Highlights/Highlights.stories.tsx index 200d2385b..5a84a0950 100644 --- a/src/components/Highlights/Highlights.stories.tsx +++ b/src/components/Highlights/Highlights.stories.tsx @@ -1,9 +1,15 @@ import { Meta, StoryObj } from "@storybook/react"; import { Highlights } from "."; +import { featureFlagMinBackendVersions } from "../../featureFlags"; +import { FeatureFlag } from "../../types"; import { actions as mainActions } from "../Main/actions"; +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 { mockedTestsData } from "./Tests/mockData"; import { mockedTopIssuesData } from "./TopIssues/mockData"; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction @@ -21,7 +27,25 @@ 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_TESTS_HIGHLIGHTS_ENABLED], + deploymentType: DeploymentType.HELM, + centralize: true + } +}; + export const Default: Story = { + decorators: [ + (Story) => ( + + + + ) + ], play: () => { window.setTimeout(() => { window.postMessage({ @@ -39,11 +63,28 @@ export const Default: Story = { action: mainActions.SET_HIGHLIGHTS_IMPACT_DATA, payload: mockedImpactData }); + window.postMessage({ + type: "digma", + action: mainActions.SET_HIGHLIGHTS_SCALING_DATA, + payload: mockedScalingData + }); + window.postMessage({ + type: "digma", + action: mainActions.SET_HIGHLIGHTS_TESTS_DATA, + payload: mockedTestsData + }); }, 1000); } }; export const Empty: Story = { + decorators: [ + (Story) => ( + + + + ) + ], play: () => { window.setTimeout(() => { window.postMessage({ @@ -61,6 +102,16 @@ export const Empty: Story = { action: mainActions.SET_HIGHLIGHTS_IMPACT_DATA, payload: { impactHighlights: [] } }); + window.postMessage({ + type: "digma", + action: mainActions.SET_HIGHLIGHTS_SCALING_DATA, + payload: { scaling: [] } + }); + window.postMessage({ + type: "digma", + action: mainActions.SET_HIGHLIGHTS_TESTS_DATA, + payload: { tests: { totalCount: 0, failedCount: 0 } } + }); }, 1000); } }; diff --git a/src/components/Highlights/Scaling/Scaling.stories.tsx b/src/components/Highlights/Scaling/Scaling.stories.tsx new file mode 100644 index 000000000..2fa541d7a --- /dev/null +++ b/src/components/Highlights/Scaling/Scaling.stories.tsx @@ -0,0 +1,101 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { Scaling } from "."; +import { featureFlagMinBackendVersions } from "../../../featureFlags"; +import { FeatureFlag } from "../../../types"; +import { actions } from "../../Main/actions"; +import { ConfigContext, initialState } from "../../common/App/ConfigContext"; +import { DeploymentType } from "../../common/App/types"; +import { mockedScalingData } from "./mockData"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + 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 Default: Story = { + decorators: [ + (Story) => ( + + + + ) + ], + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_HIGHLIGHTS_SCALING_DATA, + payload: mockedScalingData + }); + }); + } +}; + +export const Loading: Story = { + decorators: [ + (Story) => ( + + + + ) + ] +}; + +export const Empty: Story = { + decorators: [ + (Story) => ( + + + + ) + ], + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_HIGHLIGHTS_SCALING_DATA, + payload: { scaling: [] } + }); + }); + } +}; + +export const Disabled: Story = { + decorators: [ + (Story) => ( + + + + ) + ] +}; diff --git a/src/components/Highlights/Scaling/index.tsx b/src/components/Highlights/Scaling/index.tsx new file mode 100644 index 000000000..7b95bfdea --- /dev/null +++ b/src/components/Highlights/Scaling/index.tsx @@ -0,0 +1,175 @@ +import { Row, createColumnHelper } from "@tanstack/react-table"; +import { useContext, useEffect, useState } from "react"; +import { actions as globalActions } from "../../../actions"; +import { ROUTES, SCALING_ISSUE_DOCUMENTATION_URL } from "../../../constants"; +import { usePrevious } from "../../../hooks/usePrevious"; +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 [isInitialLoading, setIsInitialLoading] = useState(true); + const { data, getData } = useScalingData(); + const previousData = usePrevious(data); + const config = useContext(ConfigContext); + + useEffect(() => { + getData(); + }, []); + + useEffect(() => { + if (!previousData && data) { + setIsInitialLoading(false); + } + }, [previousData, data]); + + 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 (!config.backendInfo?.centralize) { + return ( + + } + /> + ); + } + + if (isInitialLoading) { + return ( + + ); + } + + // TODO: show the card for partial data + + if (!data || data.scaling.length === 0) { + 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..b275263be --- /dev/null +++ b/src/components/Highlights/Scaling/mockData.ts @@ -0,0 +1,49 @@ +import { InsightStatus } from "../../Insights/types"; +import { ScalingData } from "./types"; + +export const mockedScalingData: ScalingData = { + 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..0490b0b88 --- /dev/null +++ b/src/components/Highlights/Scaling/types.ts @@ -0,0 +1,18 @@ +import { Duration } from "../../../globals"; +import { EnvironmentData } from "../TopIssues/types"; + +export type EnvironmentScalingData = { + concurrency: number; + duration: Duration; +}; + +export type ScalingData = { + 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/Tests/Tests.stories.tsx b/src/components/Highlights/Tests/Tests.stories.tsx new file mode 100644 index 000000000..ca20567e3 --- /dev/null +++ b/src/components/Highlights/Tests/Tests.stories.tsx @@ -0,0 +1,81 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { Tests } from "."; +import { featureFlagMinBackendVersions } from "../../../featureFlags"; +import { FeatureFlag } from "../../../types"; +import { actions } from "../../Main/actions"; +import { ConfigContext, initialState } from "../../common/App/ConfigContext"; +import { DeploymentType } from "../../common/App/types"; +import { mockedTestsData } from "./mockData"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Highlights/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 + +const mockedConfig = { + ...initialState, + backendInfo: { + applicationVersion: + featureFlagMinBackendVersions[FeatureFlag.ARE_TESTS_HIGHLIGHTS_ENABLED], + deploymentType: DeploymentType.HELM, + centralize: true + } +}; + +export const Default: Story = { + decorators: [ + (Story) => ( + + + + ) + ], + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_HIGHLIGHTS_TESTS_DATA, + payload: mockedTestsData + }); + }); + } +}; + +export const Loading: Story = { + decorators: [ + (Story) => ( + + + + ) + ] +}; + +export const Disabled: Story = { + decorators: [ + (Story) => ( + + + + ) + ] +}; diff --git a/src/components/Highlights/Tests/index.tsx b/src/components/Highlights/Tests/index.tsx new file mode 100644 index 000000000..43da81b52 --- /dev/null +++ b/src/components/Highlights/Tests/index.tsx @@ -0,0 +1,121 @@ +import { useContext, useEffect, useState } from "react"; +import { actions as globalActions } from "../../../actions"; +import { + ROUTES, + TEST_OBSERVABILITY_DOCUMENTATION_URL +} from "../../../constants"; +import { usePrevious } from "../../../hooks/usePrevious"; +import { ChangeViewPayload } from "../../../types"; +import { openURLInDefaultBrowser } from "../../../utils/actions/openURLInDefaultBrowser"; +import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; +import { ConfigContext } from "../../common/App/ConfigContext"; +import { InfinityIcon } from "../../common/icons/16px/InfinityIcon"; +import { PaperTabletIcon } from "../../common/icons/16px/PaperTabletIcon"; +import { RefreshIcon } from "../../common/icons/16px/RefreshIcon"; +import { Button } from "../../common/v3/Button"; +import { Tag } from "../../common/v3/Tag"; +import { EmptyStateCard } from "../EmptyStateCard"; +import { Section } from "../common/Section"; +import { trackingEvents } from "../tracking"; +import * as s from "./styles"; +import { useTestsData } from "./useTestsData"; + +export const Tests = () => { + const [isInitialLoading, setIsInitialLoading] = useState(true); + const { data, getData } = useTestsData(); + const previousData = usePrevious(data); + const config = useContext(ConfigContext); + + useEffect(() => { + getData(); + }, []); + + useEffect(() => { + if (!previousData && data) { + setIsInitialLoading(false); + } + }, [previousData, data]); + + const renderContent = () => { + const handleLearnMoreButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.TESTS_CARD_LEARN_MORE_BUTTON_CLICKED + ); + + openURLInDefaultBrowser(TEST_OBSERVABILITY_DOCUMENTATION_URL); + }; + + const handleViewTestsButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.TESTS_CARD_VIEW_TESTS_BUTTON_CLICKED + ); + + window.sendMessageToDigma({ + action: globalActions.CHANGE_VIEW, + payload: { + view: ROUTES.TESTS + } + }); + }; + + if (!config.backendInfo?.centralize) { + return ( + + } + /> + ); + } + + if (isInitialLoading) { + return ( + + ); + } + + if (data) { + return ( + + + + Total + + + + Failed + + + +