From 211e8c132928f696f1de5cfb29c7fd0a7878218f Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Thu, 17 Oct 2024 16:53:54 +0200 Subject: [PATCH 1/2] Add Error occurrence chart, add error status tooltip --- .../OccurrenceChart.stories.tsx | 74 ++++++++ .../NewErrorCard/OccurrenceChart/index.tsx | 169 ++++++++++++++++++ .../NewErrorCard/OccurrenceChart/styles.ts | 34 ++++ .../NewErrorCard/OccurrenceChart/types.ts | 30 ++++ src/components/Errors/NewErrorCard/index.tsx | 85 ++++++++- src/components/Errors/NewErrorCard/styles.ts | 48 ++++- src/components/Errors/NewErrorCard/types.ts | 5 + src/components/Errors/actions.ts | 4 +- src/components/Errors/tracking.ts | 3 +- src/featureFlags.ts | 3 +- src/types.ts | 3 +- 11 files changed, 450 insertions(+), 8 deletions(-) create mode 100644 src/components/Errors/NewErrorCard/OccurrenceChart/OccurrenceChart.stories.tsx create mode 100644 src/components/Errors/NewErrorCard/OccurrenceChart/index.tsx create mode 100644 src/components/Errors/NewErrorCard/OccurrenceChart/styles.ts create mode 100644 src/components/Errors/NewErrorCard/OccurrenceChart/types.ts diff --git a/src/components/Errors/NewErrorCard/OccurrenceChart/OccurrenceChart.stories.tsx b/src/components/Errors/NewErrorCard/OccurrenceChart/OccurrenceChart.stories.tsx new file mode 100644 index 000000000..c12cd4d52 --- /dev/null +++ b/src/components/Errors/NewErrorCard/OccurrenceChart/OccurrenceChart.stories.tsx @@ -0,0 +1,74 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { OccurrenceChart } from "."; +import { actions } from "../../actions"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Errors/NewErrorCard/OccurrenceChart", + component: OccurrenceChart, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +type Story = StoryObj; + +export const Default: Story = { + play: () => { + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_ERROR_TIME_SERIES_DATA, + payload: { + dailyOccurrence: [ + { + date: "2024-10-17T00:00:00", + value: 15 + }, + { + date: "2024-10-18T00:00:00", + value: 10 + }, + { + date: "2024-10-19T00:00:00", + value: 5 + }, + { + date: "2024-10-20T00:00:00", + value: 20 + }, + { + date: "2024-10-21T00:00:00", + value: 25 + }, + { + date: "2024-10-22T00:00:00", + value: 30 + }, + { + date: "2024-10-23T00:00:00", + value: 35 + }, + { + date: "2024-10-24T00:00:00", + value: 40 + }, + { + date: "2024-10-25T00:00:00", + value: 45 + }, + { + date: "2024-10-26T00:00:00", + value: 50 + } + ] + } + }); + }, 500); + } +}; diff --git a/src/components/Errors/NewErrorCard/OccurrenceChart/index.tsx b/src/components/Errors/NewErrorCard/OccurrenceChart/index.tsx new file mode 100644 index 000000000..0e94ed7e6 --- /dev/null +++ b/src/components/Errors/NewErrorCard/OccurrenceChart/index.tsx @@ -0,0 +1,169 @@ +import { format } from "date-fns"; +import { useEffect, useMemo, useState } from "react"; +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis +} from "recharts"; +import { useTheme } from "styled-components"; +import { + DataFetcherConfiguration, + useFetchData +} from "../../../../hooks/useFetchData"; +import { useConfigSelector } from "../../../../store/config/useConfigSelector"; +import { isNumber } from "../../../../typeGuards/isNumber"; +import { actions } from "../../actions"; +import * as s from "./styles"; +import { + ErrorOccurrenceRecord, + GetErrorTimeSeriesDataPayload, + HorizontalCoordinatesGeneratorProps, + OccurrenceChartProps, + SetErrorTimeSeriesDataPayload +} from "./types"; + +const MAX_BAR_WIDTH = 32; + +export const OccurrenceChart = ({ + errorId, + spanCodeObjectId, + service +}: OccurrenceChartProps) => { + const theme = useTheme(); + const [chartData, setChartData] = useState< + SetErrorTimeSeriesDataPayload | undefined + >(); + + const { environment } = useConfigSelector(); + const environmentId = environment?.id; + + const tickLabelStyles: React.SVGProps = { + fill: theme.colors.v3.text.secondary, + opacity: 0.5 + }; + + const YAxisTickLabelStyles: React.SVGProps = { + ...tickLabelStyles, + fontSize: theme.typographies.footNote.fontSize, + fontWeight: theme.typographies.footNote.fontWeight.regular + }; + + const XAxisTickLabelStyles: React.SVGProps = { + ...tickLabelStyles, + fontSize: theme.typographies.captionOne.fontSize, + fontWeight: theme.typographies.captionOne.fontWeight.regular + }; + + const dataFetcherConfiguration: DataFetcherConfiguration = useMemo( + () => ({ + requestAction: actions.GET_ERROR_TIME_SERIES_DATA, + responseAction: actions.SET_ERROR_TIME_SERIES_DATA, + refreshOnPayloadChange: true + }), + [] + ); + + const payload: GetErrorTimeSeriesDataPayload = useMemo( + () => ({ + errorId, + scope: { + spanCodeObjectId, + service, + environment: environmentId ?? "" + } + }), + [errorId, spanCodeObjectId, service, environmentId] + ); + + const { data } = useFetchData< + GetErrorTimeSeriesDataPayload, + SetErrorTimeSeriesDataPayload + >(dataFetcherConfiguration, payload); + + useEffect(() => { + if (data && data.errorId === errorId) { + setChartData(data); + } + }, [data, errorId]); + + return ( + + + Occurrence over time + + {chartData?.dailyOccurrence && ( + + + { + if (!offset.height || !isNumber(offset.top)) { + return []; + } + let linesCount = 4; + const lines = []; + const maxTickTopOffset = offset.height + offset.top; + const interval = Math.floor(offset.height / linesCount); + let top = maxTickTopOffset - interval; + + while (linesCount) { + lines.push(top); + linesCount--; + top -= interval; + } + + return lines; + }} + /> + format(new Date(x), "MM/dd")} + /> + + + { + const payload = x.payload; + + if (!payload || payload.length === 0) { + return; + } + + const { date, value } = payload[0] + .payload as ErrorOccurrenceRecord; + + return ( + + Occurrences: {value} + {format(new Date(date), "MM/dd/yyyy")} + + ); + }} + isAnimationActive={false} + /> + + + )} + + ); +}; diff --git a/src/components/Errors/NewErrorCard/OccurrenceChart/styles.ts b/src/components/Errors/NewErrorCard/OccurrenceChart/styles.ts new file mode 100644 index 000000000..1655a5a96 --- /dev/null +++ b/src/components/Errors/NewErrorCard/OccurrenceChart/styles.ts @@ -0,0 +1,34 @@ +import styled from "styled-components"; +import { bodySemiboldTypography } from "../../../common/App/typographies"; +import { Tooltip } from "../../../common/v3/Tooltip/styles"; + +export const HEIGHT = 208; // in pixels + +export const HistogramContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + box-sizing: border-box; + width: 100%; + height: ${HEIGHT}px; + background: ${({ theme }) => theme.colors.v3.surface.primary}; + padding: 12px 0 12px 12px; + border-radius: 4px; +`; + +export const HistogramHeader = styled.div` + height: 28px; + display: flex; + align-items: center; +`; + +export const HistogramTitle = styled.span` + ${bodySemiboldTypography} + color: ${({ theme }) => theme.colors.v3.text.primary}; +`; + +export const TooltipContainer = styled(Tooltip)` + display: flex; + flex-direction: column; + gap: 8px; +`; diff --git a/src/components/Errors/NewErrorCard/OccurrenceChart/types.ts b/src/components/Errors/NewErrorCard/OccurrenceChart/types.ts new file mode 100644 index 000000000..9b16e22c2 --- /dev/null +++ b/src/components/Errors/NewErrorCard/OccurrenceChart/types.ts @@ -0,0 +1,30 @@ +import { ChartOffset } from "recharts/types/util/types"; + +export interface OccurrenceChartProps { + errorId: string; + spanCodeObjectId: string; + service: string; +} + +export interface GetErrorTimeSeriesDataPayload { + errorId: string; + scope: { + spanCodeObjectId: string; + service: string; + environment: string; + }; +} + +export interface ErrorOccurrenceRecord { + date: string; + value: number; +} + +export interface SetErrorTimeSeriesDataPayload { + errorId: string; + dailyOccurrence: ErrorOccurrenceRecord[]; +} + +export interface HorizontalCoordinatesGeneratorProps { + offset: ChartOffset; +} diff --git a/src/components/Errors/NewErrorCard/index.tsx b/src/components/Errors/NewErrorCard/index.tsx index 6b6f7490d..a544bab2f 100644 --- a/src/components/Errors/NewErrorCard/index.tsx +++ b/src/components/Errors/NewErrorCard/index.tsx @@ -1,4 +1,8 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { CSSTransition } from "react-transition-group"; +import { getFeatureFlagValue } from "../../../featureFlags"; +import { useConfigSelector } from "../../../store/config/useConfigSelector"; +import { FeatureFlag } from "../../../types"; import { changeScope } from "../../../utils/actions/changeScope"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; import { @@ -6,16 +10,32 @@ import { getEndpointKey } from "../../common/AffectedEndpointsSelector"; import { Option } from "../../common/AffectedEndpointsSelector/types"; +import { HistogramIcon } from "../../common/icons/16px/HistogramIcon"; +import { NewIconButton } from "../../common/v3/NewIconButton"; import { Tooltip } from "../../common/v3/Tooltip"; +import { TimestampKeyValue } from "../ErrorCard/TimestampKeyValue"; import { getTagType, HIGH_SEVERITY_SCORE_THRESHOLD } from "../Score"; import { trackingEvents } from "../tracking"; +import { OccurrenceChart } from "./OccurrenceChart"; import * as s from "./styles"; import { NewErrorCardProps } from "./types"; +export const getStatusString = (status: string) => + status.toLowerCase().startsWith("recent") ? "Recent" : status; + export const NewErrorCard = ({ data, onSourceLinkClick }: NewErrorCardProps) => { + const [isHistogramVisible, setIsHistogramVisible] = useState(false); + const chartContainerRef = useRef(null); + const { backendInfo } = useConfigSelector(); + + const isOccurrenceChartEnabled = getFeatureFlagValue( + backendInfo, + FeatureFlag.IS_ERROR_OCCURRENCE_CHART_ENABLED + ); + const { id, affectedEndpoints, @@ -23,7 +43,9 @@ export const NewErrorCard = ({ errorType, fromDisplayName, fromFullyQualifiedName, - status + status, + firstDetected, + lastDetected } = data; const statusTagType = getTagType(score.score); const selectorOptions: Option[] = useMemo( @@ -83,6 +105,13 @@ export const NewErrorCard = ({ }); }; + const handleHistogramButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.ERROR_CARD_HISTOGRAM_BUTTON_CLICKED + ); + setIsHistogramVisible(!isHistogramVisible); + }; + const isCritical = score.score > HIGH_SEVERITY_SCORE_THRESHOLD; const selectorValue = selectedEndpoint @@ -103,7 +132,25 @@ export const NewErrorCard = ({ {status && ( - + + Score: {score.score} + + + + } + type={statusTagType} + /> )} {selectorOptions.length > 0 && ( @@ -117,6 +164,38 @@ export const NewErrorCard = ({ /> )} + {isOccurrenceChartEnabled && selectedEndpoint && ( + + + + + + )} + {isOccurrenceChartEnabled && selectedEndpoint && ( + + + + )} ); }; diff --git a/src/components/Errors/NewErrorCard/styles.ts b/src/components/Errors/NewErrorCard/styles.ts index cb53bf98c..df2233d5e 100644 --- a/src/components/Errors/NewErrorCard/styles.ts +++ b/src/components/Errors/NewErrorCard/styles.ts @@ -5,7 +5,11 @@ import { } from "../../common/App/typographies"; import { Link } from "../../common/v3/Link"; import { Tag } from "../../common/v3/Tag"; -import { ContainerProps } from "./types"; +import { HEIGHT } from "./OccurrenceChart/styles"; +import { ContainerProps, OccurrenceChartContainerProps } from "./types"; + +export const TRANSITION_DURATION = 300; +export const chartContainerTransitionClassName = "chart-container"; export const Container = styled.div` display: flex; @@ -50,6 +54,11 @@ export const StatusTag = styled(Tag)` flex-shrink: 0; `; +export const StatusTagTooltipContainer = styled.div` + display: flex; + flex-direction: column; +`; + export const SourceLink = styled(Link)` max-width: 100%; `; @@ -62,3 +71,40 @@ export const AffectedEndpointsContainer = styled.div` gap: 4px; color: ${({ theme }) => theme.colors.v3.text.secondary}; `; + +export const OccurrenceChartContainer = styled.div` + margin-top: -8px; + overflow: hidden; + height: ${HEIGHT}px; + + ${({ $transitionClassName, $transitionDuration }) => ` + &.${$transitionClassName}-enter { + height: 0; + opacity: 0; + } + + &.${$transitionClassName}-enter-active { + height: ${HEIGHT}px; + opacity: 1; + transition: height ${$transitionDuration}ms ease-out, opacity ${$transitionDuration}ms ease-out; + } + + &.${$transitionClassName}-exit { + height: ${HEIGHT}px; + opacity: 1; + } + + &.${$transitionClassName}-exit-active { + height: 0; + opacity: 0; + transition: height ${$transitionDuration}ms ease-out, opacity ${$transitionDuration}ms ease-out; + } + `} +`; + +export const Footer = styled.div` + margin-top: -8px; + display: flex; + gap: 8px; + justify-content: flex-end; +`; diff --git a/src/components/Errors/NewErrorCard/types.ts b/src/components/Errors/NewErrorCard/types.ts index 12faf49de..b26c49966 100644 --- a/src/components/Errors/NewErrorCard/types.ts +++ b/src/components/Errors/NewErrorCard/types.ts @@ -9,3 +9,8 @@ export interface ContainerProps { $isPinned?: boolean; $isCritical?: boolean; } + +export interface OccurrenceChartContainerProps { + $transitionDuration: number; + $transitionClassName: string; +} diff --git a/src/components/Errors/actions.ts b/src/components/Errors/actions.ts index f926134af..bbe717a65 100644 --- a/src/components/Errors/actions.ts +++ b/src/components/Errors/actions.ts @@ -15,5 +15,7 @@ export const actions = addPrefix(ACTION_PREFIX, { GET_GLOBAL_ERRORS_DATA: "GET_GLOBAL_ERRORS_DATA", SET_GLOBAL_ERRORS_DATA: "SET_GLOBAL_ERRORS_DATA", GET_GLOBAL_ERRORS_FILTERS_DATA: "GET_GLOBAL_ERRORS_FILTERS_DATA", - SET_GLOBAL_ERRORS_FILTERS_DATA: "SET_GLOBAL_ERRORS_FILTERS_DATA" + SET_GLOBAL_ERRORS_FILTERS_DATA: "SET_GLOBAL_ERRORS_FILTERS_DATA", + GET_ERROR_TIME_SERIES_DATA: "GET_ERROR_TIME_SERIES_DATA", + SET_ERROR_TIME_SERIES_DATA: "SET_ERROR_TIME_SERIES_DATA" }); diff --git a/src/components/Errors/tracking.ts b/src/components/Errors/tracking.ts index 363d97647..796cd7f2b 100644 --- a/src/components/Errors/tracking.ts +++ b/src/components/Errors/tracking.ts @@ -34,7 +34,8 @@ export const trackingEvents = addPrefix( ERROR_CARD_SELECTED_AFFECTED_ENDPOINT_CHANGED: "error card selected affected endpoint changed", ERROR_CARD_AFFECTED_ENDPOINT_LINK_CLICKED: - "error card affected endpoint link clicked" + "error card affected endpoint link clicked", + ERROR_CARD_HISTOGRAM_BUTTON_CLICKED: "error card histogram button clicked" }, " " ); diff --git a/src/featureFlags.ts b/src/featureFlags.ts index 658eb2cb0..29b4a46b3 100644 --- a/src/featureFlags.ts +++ b/src/featureFlags.ts @@ -21,7 +21,8 @@ export const featureFlagMinBackendVersions: Record = { [FeatureFlag.IS_METRICS_REPORT_DATA_FILTER_ENABLED]: "0.3.122-alpha.3", [FeatureFlag.IS_METRICS_REPORT_ENDPOINT_VIEW_ENABLED]: "0.3.122-alpha.3", [FeatureFlag.ARE_GLOBAL_ERRORS_ENABLED]: "0.3.129", - [FeatureFlag.ARE_GLOBAL_ERRORS_FILTERS_ENABLED]: "0.3.140-alpha.2" + [FeatureFlag.ARE_GLOBAL_ERRORS_FILTERS_ENABLED]: "0.3.140-alpha.2", + [FeatureFlag.IS_ERROR_OCCURRENCE_CHART_ENABLED]: "0.3.141-alpha.3" }; export const getFeatureFlagValue = ( diff --git a/src/types.ts b/src/types.ts index ba53e93e0..500bdc1df 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,7 +23,8 @@ export enum FeatureFlag { IS_METRICS_REPORT_DATA_FILTER_ENABLED, IS_METRICS_REPORT_ENDPOINT_VIEW_ENABLED, ARE_GLOBAL_ERRORS_ENABLED, - ARE_GLOBAL_ERRORS_FILTERS_ENABLED + ARE_GLOBAL_ERRORS_FILTERS_ENABLED, + IS_ERROR_OCCURRENCE_CHART_ENABLED } export enum InsightType { From ced0efdac5e14707462faab7e4568cd84d7424fd Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Fri, 18 Oct 2024 11:44:29 +0200 Subject: [PATCH 2/2] Simplify conditional rendering --- src/components/Errors/NewErrorCard/index.tsx | 56 ++++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/components/Errors/NewErrorCard/index.tsx b/src/components/Errors/NewErrorCard/index.tsx index a544bab2f..115f0197b 100644 --- a/src/components/Errors/NewErrorCard/index.tsx +++ b/src/components/Errors/NewErrorCard/index.tsx @@ -165,36 +165,36 @@ export const NewErrorCard = ({ )} {isOccurrenceChartEnabled && selectedEndpoint && ( - - + - + + + + + - - - )} - {isOccurrenceChartEnabled && selectedEndpoint && ( - - - + + )} );