diff --git a/src/components/Insights/common/InsightCard/InsightCard.stories.tsx b/src/components/Insights/common/InsightCard/InsightCard.stories.tsx index 9483ed449..0c4f0c930 100644 --- a/src/components/Insights/common/InsightCard/InsightCard.stories.tsx +++ b/src/components/Insights/common/InsightCard/InsightCard.stories.tsx @@ -1,5 +1,7 @@ import { Meta, StoryObj } from "@storybook/react"; import { InsightCard } from "."; +import { ConfigContext, InitialData } from "../../../common/App/ConfigContext"; +import { DeploymentType } from "../../../common/App/types"; import { mockedEndpointNPlusOneInsight } from "../../EndpointNPlusOneInsight/mockData"; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction @@ -69,3 +71,25 @@ export const Dismissed: Story = { onOpenHistogram: undefined } }; + +export const WithNewVersion: Story = { + decorators: [ + (Story) => ( + + + + ) + ], + args: { + isAsync: true, + insight: { ...mockedEndpointNPlusOneInsight, criticality: 0.9 } + } +}; diff --git a/src/components/Insights/common/InsightCard/InsightHeader/index.tsx b/src/components/Insights/common/InsightCard/InsightHeader/index.tsx index 39dbf884b..495556a4d 100644 --- a/src/components/Insights/common/InsightCard/InsightHeader/index.tsx +++ b/src/components/Insights/common/InsightCard/InsightHeader/index.tsx @@ -1,4 +1,5 @@ import { useContext } from "react"; +import { formatTimeDistance } from "../../../../../utils/formatTimeDistance"; import { InsightTypeInfo, getInsightTypeInfo @@ -74,6 +75,14 @@ export const InsightHeader = (props: InsightHeaderProps) => { )} + {props.lastUpdateTimer && ( + + Updated: + + {formatTimeDistance(props.lastUpdateTimer)} + + + )} {props.isAsync && } diff --git a/src/components/Insights/common/InsightCard/InsightHeader/styles.ts b/src/components/Insights/common/InsightCard/InsightHeader/styles.ts index d29632416..91e20e933 100644 --- a/src/components/Insights/common/InsightCard/InsightHeader/styles.ts +++ b/src/components/Insights/common/InsightCard/InsightHeader/styles.ts @@ -1,5 +1,8 @@ import styled from "styled-components"; -import { bodySemiboldTypography } from "../../../../common/App/typographies"; +import { + bodySemiboldTypography, + footnoteRegularTypography +} from "../../../../common/App/typographies"; export const Container = styled.div` display: flex; @@ -41,3 +44,10 @@ export const BadgeContainer = styled.div` gap: 8px; height: 24px; `; + +export const Description = styled.div` + ${footnoteRegularTypography} + gap: 4px; + display: flex; + color: ${({ theme }) => theme.colors.v3.text.secondary}; +`; diff --git a/src/components/Insights/common/InsightCard/InsightHeader/types.ts b/src/components/Insights/common/InsightCard/InsightHeader/types.ts index 537e2d317..a0eba2b0e 100644 --- a/src/components/Insights/common/InsightCard/InsightHeader/types.ts +++ b/src/components/Insights/common/InsightCard/InsightHeader/types.ts @@ -11,4 +11,5 @@ export interface InsightHeaderProps { spanInfo?: SpanInfo | null; onSpanLinkClick: (spanCodeObjectId: string) => void; status?: InsightStatus; + lastUpdateTimer?: string | null; } diff --git a/src/components/Insights/common/InsightCard/RecalculateBar/RecalculateBar.stories.tsx b/src/components/Insights/common/InsightCard/RecalculateBar/RecalculateBar.stories.tsx new file mode 100644 index 000000000..d05b62ae7 --- /dev/null +++ b/src/components/Insights/common/InsightCard/RecalculateBar/RecalculateBar.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { RecalculateBar } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Insights/common/InsightCard/RecalculateBar", + component: RecalculateBar, + 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: {} +}; diff --git a/src/components/Insights/common/InsightCard/RecalculateBar/index.tsx b/src/components/Insights/common/InsightCard/RecalculateBar/index.tsx new file mode 100644 index 000000000..e8142eefb --- /dev/null +++ b/src/components/Insights/common/InsightCard/RecalculateBar/index.tsx @@ -0,0 +1,14 @@ +import { RecalculateStartedIcon } from "../../../../common/icons/16px/RecalculateStartedIcon"; +import * as s from "./styles"; + +export const RecalculateBar = () => { + return ( + + + + + Rechecking insight + Check here for updates + + ); +}; diff --git a/src/components/Insights/common/InsightCard/RecalculateBar/styles.ts b/src/components/Insights/common/InsightCard/RecalculateBar/styles.ts new file mode 100644 index 000000000..f93a177f4 --- /dev/null +++ b/src/components/Insights/common/InsightCard/RecalculateBar/styles.ts @@ -0,0 +1,37 @@ +import styled from "styled-components"; +import { + caption1RegularTypography, + footnoteBoldTypography +} from "../../../../common/App/typographies"; + +export const Container = styled.div` + ${footnoteBoldTypography} + + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.colors.v3.status.low}; + background: ${({ theme }) => theme.colors.v3.status.backgroundLow}; + display: flex; + align-items: center; + padding: 6px 8px; + gap: 4px; +`; + +export const IconContainer = styled.div` + color: ${({ theme }) => theme.colors.v3.status.low}; + display: flex; +`; + +export const Title = styled.span` + color: ${({ theme }) => theme.colors.v3.text.primary}; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +`; + +export const TicketStatus = styled.div` + color: ${({ theme }) => theme.colors.v3.text.secondary}; +`; + +export const Info = styled.span` + ${caption1RegularTypography} +`; diff --git a/src/components/Insights/common/InsightCard/index.tsx b/src/components/Insights/common/InsightCard/index.tsx index b3aef582d..118e22a3b 100644 --- a/src/components/Insights/common/InsightCard/index.tsx +++ b/src/components/Insights/common/InsightCard/index.tsx @@ -1,6 +1,5 @@ -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { isString } from "../../../../typeGuards/isString"; -import { formatTimeDistance } from "../../../../utils/formatTimeDistance"; import { TraceIcon } from "../../../common/icons/12px/TraceIcon"; import { HistogramIcon } from "../../../common/icons/16px/HistogramIcon"; import { LiveIcon } from "../../../common/icons/16px/LiveIcon"; @@ -10,18 +9,22 @@ import { CrossIcon } from "../../../common/icons/CrossIcon"; import { Button } from "../../../common/v3/Button"; import { BaseButtonProps } from "../../../common/v3/Button/types"; import { JiraButton } from "../../../common/v3/JiraButton"; -import { Link } from "../../../common/v3/Link"; import { Tooltip } from "../../../common/v3/Tooltip"; import { isEndpointInsight, isSpanInsight } from "../../typeGuards"; import { InsightHeader } from "./InsightHeader"; import * as s from "./styles"; import { InsightCardProps } from "./types"; +import { getFeatureFlagValue } from "../../../../featureFlags"; import { usePrevious } from "../../../../hooks/usePrevious"; +import { FeatureFlag } from "../../../../types"; import { sendTrackingEvent } from "../../../../utils/sendTrackingEvent"; import { Spinner } from "../../../Navigation/CodeButtonMenu/Spinner"; +import { ConfigContext } from "../../../common/App/ConfigContext"; import { trackingEvents } from "../../tracking"; +import { InsightStatus } from "../../types"; import { ProductionAffectionBar } from "./ProductionAffectionBar"; +import { RecalculateBar } from "./RecalculateBar"; import { useDismissalHandler } from "./useDismissalHandler"; const IS_NEW_TIME_LIMIT = 1000 * 60 * 10; // in milliseconds @@ -33,8 +36,14 @@ export const InsightCard = (props: InsightCardProps) => { useState(false); const { isLoading, dismiss, show } = useDismissalHandler(props.insight.id); const previousLoading = usePrevious(isLoading); + const config = useContext(ConfigContext); + const [insightStatus, setInsightStatus] = useState(props.insight.status); const isCritical = props.insight.criticality > HIGH_CRITICALITY_THRESHOLD; + // TODO: remove and refresh the insight data + useEffect(() => { + setInsightStatus(props.insight.status); + }, [props.insight.status]); useEffect(() => { if (previousLoading && !isLoading) { @@ -42,15 +51,13 @@ export const InsightCard = (props: InsightCardProps) => { } }, [isLoading, previousLoading, props.onRefresh]); - const handleRefreshLinkClick = () => { - props.onRefresh(props.insight.type); - }; - const handleRecheckButtonClick = () => { props.insight.prefixedCodeObjectId && props.onRecalculate && props.onRecalculate(props.insight.id); setIsRecalculatingStarted(true); + // TODO: handle Recheck response and refresh the insight data + setInsightStatus(InsightStatus.InEvaluation); }; const handleHistogramButtonClick = () => { @@ -65,56 +72,38 @@ export const InsightCard = (props: InsightCardProps) => { ); }; - const handleSpanLinkClick = () => { + const getRecalculateVisibilityParams = () => { + const areStartTimesEqual = + props.insight.customStartTime && + props.insight.actualStartTime && + new Date(props.insight.actualStartTime).valueOf() - + new Date(props.insight.customStartTime).valueOf() === + 0; + if ( - (isSpanInsight(props.insight) || isEndpointInsight(props.insight)) && - props.insight.spanInfo + getFeatureFlagValue(config, FeatureFlag.IS_RECALCULATE_BUBBLE_ENABLED) ) { - props.onGoToSpan(props.insight.spanInfo.spanCodeObjectId); + return { + showTimer: areStartTimesEqual, + showBanner: insightStatus === InsightStatus.InEvaluation + }; } - }; - const renderRecalculationBlock = ( - actualStartTime: string, - customStartTime: string | null, - isRecalculatingStarted: boolean - ) => { - if (!props.insight.customStartTime && !isRecalculatingStarted) { - return; - } + return { + showTimer: areStartTimesEqual, + showBanner: + !areStartTimesEqual && + props.insight.actualStartTime && + (props.insight.customStartTime || isRecalculatingStarted) + }; + }; + const handleSpanLinkClick = () => { if ( - isRecalculatingStarted || - (customStartTime && customStartTime > actualStartTime) + (isSpanInsight(props.insight) || isEndpointInsight(props.insight)) && + props.insight.spanInfo ) { - return ( - - - Applying the new time filter. Wait a few minutes and then refresh. - - - Refresh - - - ); - } - - const areStartTimesEqual = - customStartTime && - new Date(actualStartTime).valueOf() - - new Date(customStartTime).valueOf() === - 0; - - if (areStartTimesEqual) { - const title = new Date(actualStartTime).toString(); - return ( - - Data from:{" "} - - {formatTimeDistance(actualStartTime)} - - - ); + props.onGoToSpan(props.insight.spanInfo.spanCodeObjectId); } }; @@ -257,6 +246,8 @@ export const InsightCard = (props: InsightCardProps) => { ); }; + const { showBanner, showTimer } = getRecalculateVisibilityParams(); + const isNew = isString(props.insight.firstDetected) ? Date.now() - new Date(props.insight.firstDetected).valueOf() < IS_NEW_TIME_LIMIT @@ -271,13 +262,14 @@ export const InsightCard = (props: InsightCardProps) => { ? props.insight.spanInfo : undefined } - status={props.insight.status} + status={insightStatus} isNew={isNew} isAsync={props.isAsync} insightType={props.insight.type} importance={props.insight.importance} criticality={props.insight.criticality} onSpanLinkClick={handleSpanLinkClick} + lastUpdateTimer={showTimer ? props.insight.actualStartTime : null} /> } content={ @@ -288,12 +280,7 @@ export const InsightCard = (props: InsightCardProps) => { onCreateTicket={handleCreateTicketLinkClick} /> )} - {props.insight.actualStartTime && - renderRecalculationBlock( - props.insight.actualStartTime, - props.insight.customStartTime, - isRecalculatingStarted - )} + {showBanner && } {props.content} } diff --git a/src/components/common/App/ConfigContext.ts b/src/components/common/App/ConfigContext.ts index c7ff5e312..79df96b8f 100644 --- a/src/components/common/App/ConfigContext.ts +++ b/src/components/common/App/ConfigContext.ts @@ -3,7 +3,7 @@ import { isEnvironment } from "../../../typeGuards/isEnvironment"; import { isString } from "../../../typeGuards/isString"; import { ConfigContextData } from "./types"; -export const ConfigContext = createContext({ +export const InitialData = { digmaApiUrl: isString(window.digmaApiUrl) ? window.digmaApiUrl : "", digmaApiProxyPrefix: isString(window.digmaApiProxyPrefix) ? window.digmaApiProxyPrefix @@ -28,4 +28,6 @@ export const ConfigContext = createContext({ scope: undefined, isMicrometerProject: window.isMicrometerProject === true, state: undefined -}); +}; + +export const ConfigContext = createContext(InitialData); diff --git a/src/components/common/App/typographies.ts b/src/components/common/App/typographies.ts index e6f1850e8..bf71ff7d0 100644 --- a/src/components/common/App/typographies.ts +++ b/src/components/common/App/typographies.ts @@ -100,3 +100,9 @@ export const bodySemiboldTypography = css` font-weight: ${typographies.body.fontWeight.medium}; line-height: ${typographies.body.lineHeight}px; `; + +export const footnoteBoldTypography = css` + font-size: ${typographies.footNote.fontSize}px; + font-weight: ${typographies.footNote.fontWeight.bold}; + line-height: ${typographies.footNote.lineHeight}px; +`; diff --git a/src/components/common/icons/16px/RecalculateStartedIcon.tsx b/src/components/common/icons/16px/RecalculateStartedIcon.tsx new file mode 100644 index 000000000..f42a0fb0a --- /dev/null +++ b/src/components/common/icons/16px/RecalculateStartedIcon.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { useIconProps } from "../hooks"; +import { IconProps } from "../types"; + +const RecalculateStartedIconComponent = (props: IconProps) => { + const { size, color } = useIconProps(props); + + return ( + + + + + ); +}; + +export const RecalculateStartedIcon = React.memo( + RecalculateStartedIconComponent +); diff --git a/src/featureFlags.ts b/src/featureFlags.ts index e5a9257a4..acf1eb7e4 100644 --- a/src/featureFlags.ts +++ b/src/featureFlags.ts @@ -10,6 +10,7 @@ export const featureFlagMinBackendVersions: Record = { [FeatureFlag.IS_INSIGHT_TICKET_LINKAGE_ENABLED]: "v0.2.200", [FeatureFlag.IS_ASSETS_COMPLEX_FILTER_ENABLED]: "v0.2.215", [FeatureFlag.IS_INSIGHT_DISMISSAL_ENABLED]: "v0.2.238", + [FeatureFlag.IS_RECALCULATE_BUBBLE_ENABLED]: "v0.2.238", [FeatureFlag.IS_ANALYTICS_TAB_VISIBLE]: "v0.2.244" }; diff --git a/src/types.ts b/src/types.ts index df28756f6..4e0f1158a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export enum FeatureFlag { IS_INSIGHT_TICKET_LINKAGE_ENABLED, IS_ASSETS_COMPLEX_FILTER_ENABLED, IS_INSIGHT_DISMISSAL_ENABLED, + IS_RECALCULATE_BUBBLE_ENABLED, IS_ANALYTICS_TAB_VISIBLE }