From 8d7bc95d1953731116a63d065283c561b0584693 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Wed, 23 Oct 2024 16:18:47 +0300 Subject: [PATCH 01/11] Refactored dismiss panel to make it reusable --- .../insightCards/common/InsightCard/index.tsx | 71 +++++------------- .../insightCards/common/InsightCard/styles.ts | 1 + .../DismissPanel/DismissPanel.stories.tsx | 25 +++++++ src/components/common/DismissPanel/index.tsx | 72 +++++++++++++++++++ src/components/common/DismissPanel/styles.ts | 25 +++++++ src/components/common/DismissPanel/types.ts | 6 ++ src/featureFlags.ts | 3 +- src/types.ts | 3 +- 8 files changed, 150 insertions(+), 56 deletions(-) create mode 100644 src/components/common/DismissPanel/DismissPanel.stories.tsx create mode 100644 src/components/common/DismissPanel/index.tsx create mode 100644 src/components/common/DismissPanel/styles.ts create mode 100644 src/components/common/DismissPanel/types.ts diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/index.tsx b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/index.tsx index e4b5b3431..be47d9340 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/index.tsx +++ b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/index.tsx @@ -5,7 +5,7 @@ import { useConfigSelector } from "../../../../../../../store/config/useConfigSe import { isString } from "../../../../../../../typeGuards/isString"; import { sendUserActionTrackingEvent } from "../../../../../../../utils/actions/sendUserActionTrackingEvent"; import { getInsightTypeInfo } from "../../../../../../../utils/getInsightTypeInfo"; -import { Spinner } from "../../../../../../Navigation/CodeButtonMenu/Spinner"; +import { DismissPanel } from "../../../../../../common/DismissPanel"; import { CheckmarkCircleIcon } from "../../../../../../common/icons/12px/CheckmarkCircleIcon"; import { TraceIcon } from "../../../../../../common/icons/12px/TraceIcon"; import { DoubleCircleIcon } from "../../../../../../common/icons/16px/DoubleCircleIcon"; @@ -13,9 +13,7 @@ import { HistogramIcon } from "../../../../../../common/icons/16px/HistogramIcon import { PinIcon } from "../../../../../../common/icons/16px/PinIcon"; import { QuestionMark } from "../../../../../../common/icons/16px/QuestionMark"; import { RecheckIcon } from "../../../../../../common/icons/16px/RecheckIcon"; -import { CrossIcon } from "../../../../../../common/icons/CrossIcon"; import { JiraButton } from "../../../../../../common/v3/JiraButton"; -import { NewButton } from "../../../../../../common/v3/NewButton"; import { Tooltip } from "../../../../../../common/v3/Tooltip"; import { actions } from "../../../../../actions"; import { trackingEvents } from "../../../../../tracking"; @@ -52,8 +50,6 @@ export const InsightCard = ({ viewMode, mainMetric }: InsightCardProps) => { - const [isDismissConfirmationOpened, setDismissConfirmationOpened] = - useState(false); const { isDismissalChangeInProgress, dismiss, show } = useDismissal( insight.id ); @@ -159,7 +155,6 @@ export const InsightCard = ({ } ); dismiss(); - setDismissConfirmationOpened(false); }; const handleShowClick = () => { @@ -437,55 +432,23 @@ export const InsightCard = ({ } footer={ isFooterVisible ? ( - <> - {!isDismissConfirmationOpened ? ( - - {insight.isDismissible && ( - - {insight.isDismissed ? ( - - ) : ( - setDismissConfirmationOpened(true)} - /> - )} - {isDismissalChangeInProgress && } - - )} - {renderActions()} - - ) : ( - - Dismiss insight? - - setDismissConfirmationOpened(false)} - /> - - - + + {insight.isDismissible && ( + )} - + {renderActions()} + ) : undefined } /> diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/styles.ts b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/styles.ts index f6d53e7ef..fa5ca6253 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/styles.ts +++ b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/styles.ts @@ -7,6 +7,7 @@ import { StyledCardProps } from "./types"; export const InsightFooter = styled.div` display: flex; justify-content: space-between; + position: relative; `; export const Description = styled.div` diff --git a/src/components/common/DismissPanel/DismissPanel.stories.tsx b/src/components/common/DismissPanel/DismissPanel.stories.tsx new file mode 100644 index 000000000..16aa5999b --- /dev/null +++ b/src/components/common/DismissPanel/DismissPanel.stories.tsx @@ -0,0 +1,25 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; +import { DismissPanel } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "common/DismissPanel", + component: DismissPanel, + 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: { + message: "Dismiss insight?", + onDismiss: fn(), + onShow: fn() + } +}; diff --git a/src/components/common/DismissPanel/index.tsx b/src/components/common/DismissPanel/index.tsx new file mode 100644 index 000000000..f6b33893f --- /dev/null +++ b/src/components/common/DismissPanel/index.tsx @@ -0,0 +1,72 @@ +import { forwardRef, useState } from "react"; +import { usePrevious } from "../../../hooks/usePrevious"; +import { CrossIcon } from "../icons/CrossIcon"; +import { NewButton } from "../v3/NewButton"; +import { Spinner } from "../v3/Spinner"; +import * as s from "./styles"; +import { DismissPanelProps } from "./types"; + +const DismissPanelComponent = ({ + state, + onShow, + onDismiss, + message: confirmationMessage +}: DismissPanelProps) => { + const [isDismissConfirmationOpened, setDismissConfirmationOpened] = + useState(false); + const previousState = usePrevious(state); + + return ( + <> + {!isDismissConfirmationOpened ? ( + + {state === "dismissed" && ( + + )} + {state === "visible" && ( + setDismissConfirmationOpened(true)} + /> + )} + {state === "in-progress" && ( + <> + + + + )} + + ) : ( + + {confirmationMessage} + + setDismissConfirmationOpened(false)} + /> + { + setDismissConfirmationOpened(false); + onDismiss(); + }} + /> + + + )} + + ); +}; + +export const DismissPanel = forwardRef(DismissPanelComponent); diff --git a/src/components/common/DismissPanel/styles.ts b/src/components/common/DismissPanel/styles.ts new file mode 100644 index 000000000..46ba7d084 --- /dev/null +++ b/src/components/common/DismissPanel/styles.ts @@ -0,0 +1,25 @@ +import styled from "styled-components"; +import { subscriptRegularTypography } from "../App/typographies"; + +export const DismissDialog = styled.div` + display: flex; + justify-content: space-between; + padding: 7px; + align-items: center; + margin: -7px; + background: ${({ theme }) => theme.colors.v3.surface.sidePanelHeader}; + position: absolute; + width: 100%; + + ${subscriptRegularTypography} +`; + +export const DismissDialogActions = styled.div` + display: flex; + gap: 8px; +`; + +export const ButtonContainer = styled.div` + display: flex; + align-items: center; +`; diff --git a/src/components/common/DismissPanel/types.ts b/src/components/common/DismissPanel/types.ts new file mode 100644 index 000000000..be652d859 --- /dev/null +++ b/src/components/common/DismissPanel/types.ts @@ -0,0 +1,6 @@ +export interface DismissPanelProps { + state: "dismissed" | "in-progress" | "visible"; + onShow: () => void; + onDismiss: () => void; + message: string; +} diff --git a/src/featureFlags.ts b/src/featureFlags.ts index 003b645ff..01c6c444a 100644 --- a/src/featureFlags.ts +++ b/src/featureFlags.ts @@ -25,7 +25,8 @@ export const featureFlagMinBackendVersions: Record = { [FeatureFlag.IS_ERROR_OCCURRENCE_CHART_ENABLED]: "0.3.141-alpha.3", [FeatureFlag.ARE_GLOBAL_ERRORS_CRITICALITY_AND_UNHANDLED_FILTERS_ENABLED]: "0.3.145", - [FeatureFlag.IS_GLOBAL_ERROR_PIN_ENABLED]: "0.3.147" + [FeatureFlag.IS_GLOBAL_ERROR_PIN_ENABLED]: "0.3.147", + [FeatureFlag.IS_GLOBAL_ERROR_DISMISS_ENABLED]: "0.3.148" }; export const getFeatureFlagValue = ( diff --git a/src/types.ts b/src/types.ts index 3cfc5239e..49c5f9b0c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,7 +26,8 @@ export enum FeatureFlag { ARE_GLOBAL_ERRORS_FILTERS_ENABLED, IS_ERROR_OCCURRENCE_CHART_ENABLED, ARE_GLOBAL_ERRORS_CRITICALITY_AND_UNHANDLED_FILTERS_ENABLED, - IS_GLOBAL_ERROR_PIN_ENABLED + IS_GLOBAL_ERROR_PIN_ENABLED, + IS_GLOBAL_ERROR_DISMISS_ENABLED } export enum InsightType { From c614f8e32cd6a1209ced7647b678d2fa2e81dcce Mon Sep 17 00:00:00 2001 From: opoliarush Date: Wed, 23 Oct 2024 16:21:41 +0300 Subject: [PATCH 02/11] Clean-up --- .../insightCards/common/InsightCard/index.tsx | 2 +- .../DismissPanel/DismissPanel.stories.tsx | 2 +- src/components/common/DismissPanel/index.tsx | 27 ++++++++++++------- src/components/common/DismissPanel/types.ts | 2 +- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/index.tsx b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/index.tsx index be47d9340..3f0170742 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/index.tsx +++ b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/index.tsx @@ -437,7 +437,7 @@ export const InsightCard = ({ ; export const Default: Story = { args: { - message: "Dismiss insight?", + confirmationMessage: "Dismiss insight?", onDismiss: fn(), onShow: fn() } diff --git a/src/components/common/DismissPanel/index.tsx b/src/components/common/DismissPanel/index.tsx index f6b33893f..f3af46059 100644 --- a/src/components/common/DismissPanel/index.tsx +++ b/src/components/common/DismissPanel/index.tsx @@ -10,12 +10,25 @@ const DismissPanelComponent = ({ state, onShow, onDismiss, - message: confirmationMessage + confirmationMessage }: DismissPanelProps) => { const [isDismissConfirmationOpened, setDismissConfirmationOpened] = useState(false); const previousState = usePrevious(state); + const handleDismissClick = () => { + setDismissConfirmationOpened(true); + }; + + const handleConfirmationAgreed = () => { + setDismissConfirmationOpened(false); + onDismiss(); + }; + + const handleConfirmationDiscard = () => { + setDismissConfirmationOpened(false); + }; + return ( <> {!isDismissConfirmationOpened ? ( @@ -32,7 +45,7 @@ const DismissPanelComponent = ({ icon={CrossIcon} label={"Dismiss"} buttonType={"secondaryBorderless"} - onClick={() => setDismissConfirmationOpened(true)} + onClick={handleDismissClick} /> )} {state === "in-progress" && ( @@ -50,17 +63,11 @@ const DismissPanelComponent = ({ {confirmationMessage} - setDismissConfirmationOpened(false)} - /> + { - setDismissConfirmationOpened(false); - onDismiss(); - }} + onClick={handleConfirmationAgreed} /> diff --git a/src/components/common/DismissPanel/types.ts b/src/components/common/DismissPanel/types.ts index be652d859..c6debe086 100644 --- a/src/components/common/DismissPanel/types.ts +++ b/src/components/common/DismissPanel/types.ts @@ -2,5 +2,5 @@ export interface DismissPanelProps { state: "dismissed" | "in-progress" | "visible"; onShow: () => void; onDismiss: () => void; - message: string; + confirmationMessage: string; } From fd45b7e52090602f88a959813a8df8d301547bd9 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Wed, 23 Oct 2024 17:00:13 +0300 Subject: [PATCH 03/11] Added dismiss to error card --- .../Errors/GlobalErrorsList/types.ts | 1 + .../Errors/NewErrorCard/hooks/types.ts | 7 ++ .../Errors/NewErrorCard/hooks/useDismissal.ts | 71 +++++++++++++++++++ src/components/Errors/NewErrorCard/index.tsx | 45 +++++++++++- src/components/Errors/actions.ts | 6 +- src/components/Errors/tracking.ts | 5 +- 6 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 src/components/Errors/NewErrorCard/hooks/types.ts create mode 100644 src/components/Errors/NewErrorCard/hooks/useDismissal.ts diff --git a/src/components/Errors/GlobalErrorsList/types.ts b/src/components/Errors/GlobalErrorsList/types.ts index 0c21511c9..47762a652 100644 --- a/src/components/Errors/GlobalErrorsList/types.ts +++ b/src/components/Errors/GlobalErrorsList/types.ts @@ -42,6 +42,7 @@ export interface GlobalErrorData { }; }; pinnedAt?: string; + isDismissed?: boolean; } export interface SetGlobalErrorsDataPayload { diff --git a/src/components/Errors/NewErrorCard/hooks/types.ts b/src/components/Errors/NewErrorCard/hooks/types.ts new file mode 100644 index 000000000..b46a1d5ec --- /dev/null +++ b/src/components/Errors/NewErrorCard/hooks/types.ts @@ -0,0 +1,7 @@ +export interface DismissPayload { + id: string; +} + +export interface UndismissPayload { + id: string; +} diff --git a/src/components/Errors/NewErrorCard/hooks/useDismissal.ts b/src/components/Errors/NewErrorCard/hooks/useDismissal.ts new file mode 100644 index 000000000..6ec2a7a00 --- /dev/null +++ b/src/components/Errors/NewErrorCard/hooks/useDismissal.ts @@ -0,0 +1,71 @@ +import { useEffect, useState } from "react"; +import { dispatcher } from "../../../../dispatcher"; +import { actions } from "../../actions"; +import { DismissPayload, UndismissPayload } from "./types"; + +export const useDismissal = (id: string) => { + const [isDismissalChangeInProgress, setIsDismissalChangeInProgress] = + useState(false); + + useEffect(() => { + const handleDismissed = (data: unknown) => { + if (id === (data as DismissPayload).id) { + setIsDismissalChangeInProgress(false); + } + }; + + dispatcher.addActionListener( + actions.SET_DISMISS_ERROR_RESULT, + handleDismissed + ); + + return () => { + dispatcher.removeActionListener( + actions.SET_DISMISS_ERROR_RESULT, + handleDismissed + ); + }; + }, [id]); + + useEffect(() => { + const handleUndismissed = (data: unknown) => { + if (id === (data as UndismissPayload).id) { + setIsDismissalChangeInProgress(false); + } + }; + + dispatcher.addActionListener( + actions.SET_UNDISMISS_ERROR_RESULT, + handleUndismissed + ); + + return () => { + dispatcher.removeActionListener( + actions.SET_UNDISMISS_ERROR_RESULT, + handleUndismissed + ); + }; + }, [id]); + + return { + isDismissalChangeInProgress, + dismiss: () => { + window.sendMessageToDigma({ + action: actions.DISMISS_ERROR, + payload: { + id + } + }); + setIsDismissalChangeInProgress(true); + }, + show: () => { + window.sendMessageToDigma({ + action: actions.UNDISMISS_ERROR, + payload: { + id + } + }); + setIsDismissalChangeInProgress(true); + } + }; +}; diff --git a/src/components/Errors/NewErrorCard/index.tsx b/src/components/Errors/NewErrorCard/index.tsx index 840743863..ba2d5b015 100644 --- a/src/components/Errors/NewErrorCard/index.tsx +++ b/src/components/Errors/NewErrorCard/index.tsx @@ -10,6 +10,7 @@ import { getEndpointKey } from "../../common/AffectedEndpointsSelector"; import { Option } from "../../common/AffectedEndpointsSelector/types"; +import { DismissPanel } from "../../common/DismissPanel"; import { HistogramIcon } from "../../common/icons/16px/HistogramIcon"; import { PinFillIcon } from "../../common/icons/16px/PinFillIcon"; import { PinIcon } from "../../common/icons/16px/PinIcon"; @@ -19,6 +20,7 @@ import { actions } from "../actions"; import { TimestampKeyValue } from "../ErrorCard/TimestampKeyValue"; import { getTagType, HIGH_SEVERITY_SCORE_THRESHOLD } from "../Score"; import { trackingEvents } from "../tracking"; +import { useDismissal } from "./hooks/useDismissal"; import { OccurrenceChart } from "./OccurrenceChart"; import * as s from "./styles"; import { NewErrorCardProps, PinErrorPayload, UnpinErrorPayload } from "./types"; @@ -44,6 +46,11 @@ export const NewErrorCard = ({ FeatureFlag.IS_GLOBAL_ERROR_PIN_ENABLED ); + const isDismissEnabled = getFeatureFlagValue( + backendInfo, + FeatureFlag.IS_GLOBAL_ERROR_DISMISS_ENABLED + ); + const { id, affectedEndpoints, @@ -53,9 +60,11 @@ export const NewErrorCard = ({ fromFullyQualifiedName, status, firstDetected, - lastDetected + lastDetected, + isDismissed } = data; const statusTagType = getTagType(score.score); + const { isDismissalChangeInProgress, dismiss, show } = useDismissal(id); const selectorOptions: Option[] = useMemo( () => affectedEndpoints.map((x) => ({ @@ -152,6 +161,20 @@ export const NewErrorCard = ({ } }; + const handleDismissalButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.ERROR_CARD_DISMISS_BUTTON_CLICKED + ); + dismiss(); + }; + + const handleUndismissalButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.ERROR_CARD_PIN_UNDISMISS_BUTTON_CLICKED + ); + show(); + }; + const isCritical = score.score > HIGH_SEVERITY_SCORE_THRESHOLD; const isPinned = Boolean(data.pinnedAt); @@ -254,7 +277,25 @@ export const NewErrorCard = ({ )} - {toolbarActions.length > 0 && {toolbarActions}} + {toolbarActions.length > 0 && ( + + {isDismissEnabled && ( + + )} + {toolbarActions} + + )} ); }; diff --git a/src/components/Errors/actions.ts b/src/components/Errors/actions.ts index ea8f49364..6772a9ced 100644 --- a/src/components/Errors/actions.ts +++ b/src/components/Errors/actions.ts @@ -20,6 +20,10 @@ export const actions = addPrefix(ACTION_PREFIX, { SET_ERROR_TIME_SERIES_DATA: "SET_ERROR_TIME_SERIES_DATA", PIN_ERROR: "PIN_ERROR", SET_PIN_ERROR_RESULT: "SET_PIN_ERROR_RESULT", + SET_UNPIN_ERROR_RESULT: "SET_UNPIN_ERROR_RESULT", UNPIN_ERROR: "UNPIN_ERROR", - SET_UNPIN_ERROR_RESULT: "SET_UNPIN_ERROR_RESULT" + DISMISS_ERROR: "DISMISS_ERROR", + UNDISMISS_ERROR: "UNDISMISS_ERROR", + SET_DISMISS_ERROR_RESULT: "SET_DISMISS_ERROR_RESULT", + SET_UNDISMISS_ERROR_RESULT: "SET_UNDISMISS_ERROR_RESULT" }); diff --git a/src/components/Errors/tracking.ts b/src/components/Errors/tracking.ts index 69521eeae..aa28fe0e8 100644 --- a/src/components/Errors/tracking.ts +++ b/src/components/Errors/tracking.ts @@ -40,7 +40,10 @@ export const trackingEvents = addPrefix( ERROR_CARD_AFFECTED_ENDPOINT_LINK_CLICKED: "error card affected endpoint link clicked", ERROR_CARD_HISTOGRAM_BUTTON_CLICKED: "error card histogram button clicked", - ERROR_CARD_PIN_UNPIN_BUTTON_CLICKED: "error card pin unpin button clicked" + ERROR_CARD_PIN_UNPIN_BUTTON_CLICKED: "error card pin unpin button clicked", + ERROR_CARD_DISMISS_BUTTON_CLICKED: "error card dismiss button clicked", + ERROR_CARD_PIN_UNDISMISS_BUTTON_CLICKED: + "error card pin undismiss button clicked" }, " " ); From 87ddd83edee700cd0f8258193a1a9748b7168b08 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Thu, 24 Oct 2024 17:19:32 +0300 Subject: [PATCH 04/11] added dismiss for error page --- .../GlobalErrorsList.stories.tsx | 34 +++++- .../Errors/GlobalErrorsList/index.tsx | 74 ++++++++++++- .../Errors/GlobalErrorsList/mockData.ts | 103 ++++++++++++++++++ .../Errors/GlobalErrorsList/styles.ts | 56 +++++++++- .../Errors/GlobalErrorsList/types.ts | 7 ++ src/components/common/icons/16px/EyeIcon.tsx | 6 +- src/components/common/v3/Pagination/index.tsx | 14 ++- src/components/common/v3/Pagination/styles.ts | 8 +- src/components/common/v3/Pagination/types.ts | 3 + src/store/errors/errorsSlice.ts | 9 ++ 10 files changed, 299 insertions(+), 15 deletions(-) create mode 100644 src/components/Errors/GlobalErrorsList/mockData.ts diff --git a/src/components/Errors/GlobalErrorsList/GlobalErrorsList.stories.tsx b/src/components/Errors/GlobalErrorsList/GlobalErrorsList.stories.tsx index 3be8cc78b..36ba46cf5 100644 --- a/src/components/Errors/GlobalErrorsList/GlobalErrorsList.stories.tsx +++ b/src/components/Errors/GlobalErrorsList/GlobalErrorsList.stories.tsx @@ -1,6 +1,10 @@ import { Meta, StoryObj } from "@storybook/react"; import { GlobalErrorsList } from "."; +import { ViewMode } from "../../../store/errors/errorsSlice"; +import { useStore } from "../../../store/useStore"; +import { actions } from "../actions"; +import { DefaultErrorList, DismissedErrorList } from "./mockData"; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction const meta: Meta = { @@ -17,4 +21,32 @@ 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 = {}; +export const Default: Story = { + play: () => { + const { setEnvironment } = useStore.getState(); + setEnvironment({ id: "test-env-id", name: "test", type: "Public" }); + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_GLOBAL_ERRORS_DATA, + payload: DefaultErrorList + }); + }, 100); + } +}; + +export const Dismissed: Story = { + play: () => { + const { setEnvironment, setGlobalErrorsViewMode } = useStore.getState(); + setEnvironment({ id: "test-env-id", name: "test", type: "Public" }); + setGlobalErrorsViewMode(ViewMode.OnlyDismissed); + + window.setTimeout(() => { + window.postMessage({ + type: "digma", + action: actions.SET_GLOBAL_ERRORS_DATA, + payload: DismissedErrorList + }); + }, 100); + } +}; diff --git a/src/components/Errors/GlobalErrorsList/index.tsx b/src/components/Errors/GlobalErrorsList/index.tsx index 65b1df20a..4d2023f30 100644 --- a/src/components/Errors/GlobalErrorsList/index.tsx +++ b/src/components/Errors/GlobalErrorsList/index.tsx @@ -9,14 +9,19 @@ import { useMount } from "../../../hooks/useMount"; import { useConfigSelector } from "../../../store/config/useConfigSelector"; import { GLOBAL_ERROR_SORTING_CRITERION, - PAGE_SIZE + PAGE_SIZE, + ViewMode } from "../../../store/errors/errorsSlice"; import { useErrorsSelector } from "../../../store/errors/useErrorsSelector"; import { useStore } from "../../../store/useStore"; +import { isNumber } from "../../../typeGuards/isNumber"; import { FeatureFlag } from "../../../types"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; +import { formatUnit } from "../../../utils/formatUnit"; import { OppositeArrowsIcon } from "../../common/icons/12px/OppositeArrowsIcon"; +import { ChevronIcon } from "../../common/icons/16px/ChevronIcon"; import { CardsColoredIcon } from "../../common/icons/CardsColoredIcon"; +import { Direction } from "../../common/icons/types"; import { NewPopover } from "../../common/NewPopover"; import { SearchInput } from "../../common/SearchInput"; import { NewButton } from "../../common/v3/NewButton"; @@ -44,6 +49,7 @@ export const GlobalErrorsList = () => { const { environment, backendInfo } = useConfigSelector(); const { + globalErrorsViewMode: mode, globalErrorsSearch: search, globalErrorsSorting: sorting, globalErrorsPage: page, @@ -59,7 +65,8 @@ export const GlobalErrorsList = () => { setGlobalErrorsSorting, setGlobalErrorsPage, resetGlobalErrors, - resetGlobalErrorsSelectedFilters + resetGlobalErrorsSelectedFilters, + setGlobalErrorsViewMode } = useStore.getState(); const areGlobalErrorsFiltersEnabled = getFeatureFlagValue( @@ -101,6 +108,7 @@ export const GlobalErrorsList = () => { sortBy: sorting, page, pageSize: PAGE_SIZE, + dismissed: mode === ViewMode.OnlyDismissed, ...(areGlobalErrorsFiltersEnabled ? { services: selectedFilters.services, @@ -120,13 +128,14 @@ export const GlobalErrorsList = () => { search, sorting, page, + mode, areGlobalErrorsFiltersEnabled, - areGlobalErrorsCriticalityAndUnhandledFiltersEnabled, selectedFilters.services, selectedFilters.endpoints, selectedFilters.errorTypes, selectedFilters.criticalities, - selectedFilters.handlingTypes + selectedFilters.handlingTypes, + areGlobalErrorsCriticalityAndUnhandledFiltersEnabled ] ); @@ -135,14 +144,27 @@ export const GlobalErrorsList = () => { SetGlobalErrorsDataPayload >(dataFetcherConfiguration, payload); + const isDismissalViewModeButtonVisible = + data?.dismissedCount && data.dismissedCount > 0; // isUndefined - check for backward compatibility, always show when BE does not return this counter + // Refresh data after pin/unpin actions useEffect(() => { + dispatcher.addActionListener(actions.SET_UNDISMISS_ERROR_RESULT, getData); + dispatcher.addActionListener(actions.SET_DISMISS_ERROR_RESULT, getData); dispatcher.addActionListener(actions.SET_PIN_ERROR_RESULT, getData); dispatcher.addActionListener(actions.SET_UNPIN_ERROR_RESULT, getData); return () => { dispatcher.removeActionListener(actions.SET_PIN_ERROR_RESULT, getData); dispatcher.removeActionListener(actions.SET_UNPIN_ERROR_RESULT, getData); + dispatcher.removeActionListener( + actions.SET_UNDISMISS_ERROR_RESULT, + getData + ); + dispatcher.removeActionListener( + actions.SET_DISMISS_ERROR_RESULT, + getData + ); }; }, [getData]); @@ -226,6 +248,16 @@ export const GlobalErrorsList = () => { resetGlobalErrorsSelectedFilters(); }; + const handleDismissalViewModeButtonClick = () => { + const newMode = + mode === ViewMode.All ? ViewMode.OnlyDismissed : ViewMode.All; + setGlobalErrorsViewMode(newMode); + }; + + const handleBackToAllInsightsButtonClick = () => { + setGlobalErrorsViewMode(ViewMode.All); + }; + const areAnyFiltersApplied = search || [ @@ -238,6 +270,24 @@ export const GlobalErrorsList = () => { return ( + {mode === ViewMode.OnlyDismissed && isNumber(data?.dismissedCount) && ( + + + + + + Back to All Errors + + + {data?.dismissedCount} + dismissed {formatUnit(data?.dismissedCount ?? 0, "error")} + + + )} {list ? ( <> @@ -283,7 +333,21 @@ export const GlobalErrorsList = () => { onPageChange={handlePageChange} extendedNavigation={true} withDescription={true} - /> + > + {isDismissalViewModeButtonVisible && ( + ( + + )} + onClick={handleDismissalViewModeButtonClick} + /> + )} + ) : areAnyFiltersApplied ? ( theme.colors.v3.text.tertiary}; + gap: 5px; +`; + +export const Count = styled.span` + color: ${({ theme }) => theme.colors.v3.text.primary}; +`; + +export const ToolbarRow = styled.div` + display: flex; + gap: 4px; + justify-content: space-between; + align-items: center; +`; + +export const ViewModeToolbarRow = styled(ToolbarRow)` + padding: 4px 0; +`; + +export const BackToAllErrorsButtonIconContainer = styled.div` + display: flex; + color: ${({ theme }) => theme.colors.v3.icon.disabled}; +`; + +export const BackToAllErrorsButton = styled.button` + ${subscriptRegularTypography} + + font-family: inherit; + padding: 0; + margin: 0; + background: none; + border: none; + display: flex; + gap: 4px; + align-items: center; + cursor: pointer; + color: ${({ theme }) => theme.colors.v3.text.primary}; +`; + export const ToolbarContainer = styled.div` display: flex; gap: 4px; @@ -45,3 +94,8 @@ export const EmptyStateContent = styled.div` export const SortButtonIconContainer = styled.div` color: ${({ theme }) => theme.colors.v3.icon.tertiary}; `; + +export const DismissBtnIcon = styled(EyeIcon)` + color: ${({ theme, $isDismissedMode }) => + $isDismissedMode ? theme.colors.v3.icon.brandSecondary : "currentColor"}; +`; diff --git a/src/components/Errors/GlobalErrorsList/types.ts b/src/components/Errors/GlobalErrorsList/types.ts index 47762a652..47a2cb1a4 100644 --- a/src/components/Errors/GlobalErrorsList/types.ts +++ b/src/components/Errors/GlobalErrorsList/types.ts @@ -16,6 +16,7 @@ export interface GetGlobalErrorsDataPayload { errorTypes?: string[]; criticalities?: ErrorCriticality[]; handlingTypes?: ErrorHandlingType[]; + dismissed?: boolean; } export interface GlobalErrorData { @@ -43,9 +44,15 @@ export interface GlobalErrorData { }; pinnedAt?: string; isDismissed?: boolean; + unhandled?: boolean; } export interface SetGlobalErrorsDataPayload { totalCount: number; + dismissedCount?: number; list: GlobalErrorData[]; } + +export interface DismissBtnIconProps { + $isDismissedMode: boolean; +} diff --git a/src/components/common/icons/16px/EyeIcon.tsx b/src/components/common/icons/16px/EyeIcon.tsx index 7485e7a5f..0c1a593a3 100644 --- a/src/components/common/icons/16px/EyeIcon.tsx +++ b/src/components/common/icons/16px/EyeIcon.tsx @@ -1,9 +1,10 @@ -import React from "react"; +import { forwardRef } from "react"; import { useIconProps } from "../hooks"; import { IconProps } from "../types"; interface EyeIconProps extends IconProps { crossOut?: boolean; + className?: string; } const EyeIconComponent = (props: EyeIconProps) => { @@ -14,6 +15,7 @@ const EyeIconComponent = (props: EyeIconProps) => { xmlns="http://www.w3.org/2000/svg" width={size} height={size} + className={props.className} viewBox="0 0 16 12" fill="none" > @@ -32,4 +34,4 @@ const EyeIconComponent = (props: EyeIconProps) => { ); }; -export const EyeIcon = React.memo(EyeIconComponent); +export const EyeIcon = forwardRef(EyeIconComponent); diff --git a/src/components/common/v3/Pagination/index.tsx b/src/components/common/v3/Pagination/index.tsx index aa18cc336..a1102b9be 100644 --- a/src/components/common/v3/Pagination/index.tsx +++ b/src/components/common/v3/Pagination/index.tsx @@ -15,7 +15,8 @@ export const Pagination = ({ onPageChange, extendedNavigation, withDescription, - trackingEventPrefix = "" + trackingEventPrefix = "", + children }: PaginationProps) => { const prefixedGlobalTrackingEvents = addPrefix( trackingEventPrefix, @@ -45,10 +46,13 @@ export const Pagination = ({ {(pageCount > 1 || extendedNavigation) && ( {withDescription && ( - - Showing {pageStartItemNumber} - {pageEndItemNumber} of{" "} - {itemsCount} {formatUnit(itemsCount, "Result")} - + + + Showing {pageStartItemNumber} - {pageEndItemNumber} of{" "} + {itemsCount} {formatUnit(itemsCount, "Result")} + + {children} + )} {extendedNavigation && ( diff --git a/src/components/common/v3/Pagination/styles.ts b/src/components/common/v3/Pagination/styles.ts index 86bb248a8..e093c662a 100644 --- a/src/components/common/v3/Pagination/styles.ts +++ b/src/components/common/v3/Pagination/styles.ts @@ -9,8 +9,14 @@ export const Container = styled.div` gap: 8px; `; -export const Description = styled.span` +export const DescriptionContainer = styled.div` + gap: 8px; + display: flex; margin-right: auto; + align-items: center; +`; + +export const Description = styled.span` color: ${({ theme }) => theme.colors.v3.text.tertiary}; `; diff --git a/src/components/common/v3/Pagination/types.ts b/src/components/common/v3/Pagination/types.ts index bb74b362d..a9e2d6fdc 100644 --- a/src/components/common/v3/Pagination/types.ts +++ b/src/components/common/v3/Pagination/types.ts @@ -1,3 +1,5 @@ +import { ReactNode } from "react"; + export interface PaginationProps { itemsCount: number; page: number; @@ -6,4 +8,5 @@ export interface PaginationProps { extendedNavigation?: boolean; withDescription?: boolean; trackingEventPrefix?: string; + children?: ReactNode; } diff --git a/src/store/errors/errorsSlice.ts b/src/store/errors/errorsSlice.ts index b1b8ec9ca..1ad9d8961 100644 --- a/src/store/errors/errorsSlice.ts +++ b/src/store/errors/errorsSlice.ts @@ -12,6 +12,11 @@ export enum GLOBAL_ERROR_SORTING_CRITERION { LATEST = "Latest" } +export enum ViewMode { + All = "All", + OnlyDismissed = "OnlyDismissed" +} + export type ErrorFilter = "Services" | "Endpoints" | "ErrorTypes"; export type ErrorHandlingType = "Handled" | "Unhandled"; export type ErrorCriticality = "High" | "Medium" | "Low"; @@ -40,6 +45,7 @@ export interface ErrorsState { globalErrorsSorting: GLOBAL_ERROR_SORTING_CRITERION; globalErrorsFilters: GlobalErrorsFiltersState; globalErrorsSelectedFilters: GlobalErrorsSelectedFiltersState; + globalErrorsViewMode: ViewMode; errorDetailsWorkspaceItemsOnly: boolean; } @@ -64,6 +70,7 @@ export const globalErrorsInitialState = { endpoints: null, errorTypes: null }, + globalErrorsViewMode: ViewMode.All, globalErrorsSelectedFilters: selectedFiltersInitialState }; @@ -105,6 +112,8 @@ export const errorsSlice = createSlice({ setErrorDetailsWorkspaceItemsOnly: ( errorDetailsWorkspaceItemsOnly: boolean ) => set({ errorDetailsWorkspaceItemsOnly }), + setGlobalErrorsViewMode: (mode: ViewMode) => + set({ globalErrorsViewMode: mode }), resetGlobalErrors: () => set({ ...globalErrorsInitialState }) } }); From 969cc26f978df057b9a855d9021610d4f538d828 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Wed, 23 Oct 2024 15:56:00 +0200 Subject: [PATCH 05/11] Add Pin/Unpin animations --- package-lock.json | 6 ++ package.json | 1 + .../Errors/GlobalErrorsList/index.tsx | 71 +++++++++++++++++-- .../Errors/GlobalErrorsList/types.ts | 8 +++ .../NewErrorCard/NewErrorCard.stories.tsx | 9 +++ src/components/Errors/NewErrorCard/index.tsx | 19 +++-- src/components/Errors/NewErrorCard/styles.ts | 4 ++ src/components/Errors/NewErrorCard/types.ts | 8 +-- 8 files changed, 107 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index e3a67db83..8448bf711 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@floating-ui/react": "^0.25.1", + "@formkit/auto-animate": "^0.8.2", "@tanstack/react-table": "^8.7.8", "allotment": "^1.19.0", "axios": "^1.7.4", @@ -2646,6 +2647,11 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, + "node_modules/@formkit/auto-animate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.2.tgz", + "integrity": "sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", diff --git a/package.json b/package.json index e4f61729f..29ff8a2b1 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ }, "dependencies": { "@floating-ui/react": "^0.25.1", + "@formkit/auto-animate": "^0.8.2", "@tanstack/react-table": "^8.7.8", "allotment": "^1.19.0", "axios": "^1.7.4", diff --git a/src/components/Errors/GlobalErrorsList/index.tsx b/src/components/Errors/GlobalErrorsList/index.tsx index 4d2023f30..06747bfab 100644 --- a/src/components/Errors/GlobalErrorsList/index.tsx +++ b/src/components/Errors/GlobalErrorsList/index.tsx @@ -1,4 +1,5 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { dispatcher } from "../../../dispatcher"; import { getFeatureFlagValue } from "../../../featureFlags"; import { @@ -6,6 +7,7 @@ import { useFetchData } from "../../../hooks/useFetchData"; import { useMount } from "../../../hooks/useMount"; +import { usePrevious } from "../../../hooks/usePrevious"; import { useConfigSelector } from "../../../store/config/useConfigSelector"; import { GLOBAL_ERROR_SORTING_CRITERION, @@ -38,13 +40,29 @@ import { GlobalErrorsFilters } from "./GlobalErrorsFilters"; import * as s from "./styles"; import { GetGlobalErrorsDataPayload, - SetGlobalErrorsDataPayload + SetGlobalErrorsDataPayload, + SetPinUnpinErrorResultPayload } from "./types"; +const PIN_UNPIN_ANIMATION_DURATION = 250; + export const GlobalErrorsList = () => { const { goTo } = useHistory(); const [isSortingMenuOpen, setIsSortingMenuOpen] = useState(false); const listContainerRef = useRef(null); + const [parent, toggleAnimations] = useAutoAnimate({ + duration: PIN_UNPIN_ANIMATION_DURATION + }); + const [latestPinChangedId, setLatestPinChangedId] = useState(); + + // useAutoAnimate requires to memoize callback + const getListContainerRef = useCallback( + (el: HTMLDivElement | null) => { + listContainerRef.current = el; + parent(el); + }, + [parent, listContainerRef] + ); const { environment, backendInfo } = useConfigSelector(); @@ -59,6 +77,8 @@ export const GlobalErrorsList = () => { globalErrorsSelectedFilters: selectedFilters } = useErrorsSelector(); + const previousList = usePrevious(list); + const { setGlobalErrorsData, setGlobalErrorsSearch, @@ -151,8 +171,19 @@ export const GlobalErrorsList = () => { useEffect(() => { dispatcher.addActionListener(actions.SET_UNDISMISS_ERROR_RESULT, getData); dispatcher.addActionListener(actions.SET_DISMISS_ERROR_RESULT, getData); - dispatcher.addActionListener(actions.SET_PIN_ERROR_RESULT, getData); - dispatcher.addActionListener(actions.SET_UNPIN_ERROR_RESULT, getData); + const handlePinUnpinResult = (data: unknown) => { + const payload = data as SetPinUnpinErrorResultPayload; + setLatestPinChangedId(payload.id); + getData(); + }; + dispatcher.addActionListener( + actions.SET_PIN_ERROR_RESULT, + handlePinUnpinResult + ); + dispatcher.addActionListener( + actions.SET_UNPIN_ERROR_RESULT, + handlePinUnpinResult + ); return () => { dispatcher.removeActionListener(actions.SET_PIN_ERROR_RESULT, getData); @@ -168,8 +199,9 @@ export const GlobalErrorsList = () => { }; }, [getData]); - // Cleanup errors store slice on unmount useMount(() => { + toggleAnimations(false); + return () => { resetGlobalErrors(); }; @@ -182,6 +214,28 @@ export const GlobalErrorsList = () => { } }, [data, setGlobalErrorsData]); + // Disable animations after pin/unpin + useEffect(() => { + if (!previousList || !list || !latestPinChangedId) { + return; + } + + if (previousList !== list) { + const isLatestChangedIdInList = list.some( + (x) => x.id === latestPinChangedId + ); + + if (isLatestChangedIdInList) { + setTimeout(() => { + toggleAnimations(false); + }, PIN_UNPIN_ANIMATION_DURATION); + } else { + toggleAnimations(false); + } + setLatestPinChangedId(undefined); + } + }, [previousList, list, latestPinChangedId, toggleAnimations]); + // Reset page on filters change useEffect(() => { setGlobalErrorsPage(0); @@ -258,6 +312,10 @@ export const GlobalErrorsList = () => { setGlobalErrorsViewMode(ViewMode.All); }; + const handlePinChange = () => { + toggleAnimations(true); + }; + const areAnyFiltersApplied = search || [ @@ -317,12 +375,13 @@ export const GlobalErrorsList = () => { {list.length > 0 ? ( <> - + {list.map((x) => ( ))} diff --git a/src/components/Errors/GlobalErrorsList/types.ts b/src/components/Errors/GlobalErrorsList/types.ts index 47a2cb1a4..d2c8d5067 100644 --- a/src/components/Errors/GlobalErrorsList/types.ts +++ b/src/components/Errors/GlobalErrorsList/types.ts @@ -53,6 +53,14 @@ export interface SetGlobalErrorsDataPayload { list: GlobalErrorData[]; } +export interface SetPinUnpinErrorResultPayload { + id: string; + status: "success" | "failure"; + error?: { + message: string; + }; +} + export interface DismissBtnIconProps { $isDismissedMode: boolean; } diff --git a/src/components/Errors/NewErrorCard/NewErrorCard.stories.tsx b/src/components/Errors/NewErrorCard/NewErrorCard.stories.tsx index 29c8fb2f2..50f57f39f 100644 --- a/src/components/Errors/NewErrorCard/NewErrorCard.stories.tsx +++ b/src/components/Errors/NewErrorCard/NewErrorCard.stories.tsx @@ -35,3 +35,12 @@ export const Critical: Story = { } } }; + +export const Pinned: Story = { + args: { + data: { + ...mockedGlobalErrorData, + pinnedAt: "2024-10-06T12:57:46.864939Z" + } + } +}; diff --git a/src/components/Errors/NewErrorCard/index.tsx b/src/components/Errors/NewErrorCard/index.tsx index ba2d5b015..d95631d31 100644 --- a/src/components/Errors/NewErrorCard/index.tsx +++ b/src/components/Errors/NewErrorCard/index.tsx @@ -23,14 +23,15 @@ import { trackingEvents } from "../tracking"; import { useDismissal } from "./hooks/useDismissal"; import { OccurrenceChart } from "./OccurrenceChart"; import * as s from "./styles"; -import { NewErrorCardProps, PinErrorPayload, UnpinErrorPayload } from "./types"; +import { NewErrorCardProps, PinUnpinErrorPayload } from "./types"; export const getStatusString = (status: string) => status.toLowerCase().startsWith("recent") ? "Recent" : status; export const NewErrorCard = ({ data, - onSourceLinkClick + onSourceLinkClick, + onPinChange }: NewErrorCardProps) => { const [isHistogramVisible, setIsHistogramVisible] = useState(false); const chartContainerRef = useRef(null); @@ -143,7 +144,7 @@ export const NewErrorCard = ({ } if (value) { - window.sendMessageToDigma({ + window.sendMessageToDigma({ action: actions.PIN_ERROR, payload: { id, @@ -151,7 +152,7 @@ export const NewErrorCard = ({ } }); } else { - window.sendMessageToDigma({ + window.sendMessageToDigma({ action: actions.UNPIN_ERROR, payload: { id, @@ -159,6 +160,7 @@ export const NewErrorCard = ({ } }); } + onPinChange(); }; const handleDismissalButtonClick = () => { @@ -225,17 +227,20 @@ export const NewErrorCard = ({ content={getStatusString(status)} title={ - Score: {score.score} + + Score:{" "} + {score.score} + } type={statusTagType} diff --git a/src/components/Errors/NewErrorCard/styles.ts b/src/components/Errors/NewErrorCard/styles.ts index a74f4eb59..bdb2a8060 100644 --- a/src/components/Errors/NewErrorCard/styles.ts +++ b/src/components/Errors/NewErrorCard/styles.ts @@ -64,6 +64,10 @@ export const StatusTagTooltipContainer = styled.div` flex-direction: column; `; +export const StatusTagTooltipKey = styled.span` + color: ${({ theme }) => theme.colors.v3.text.tertiary}; +`; + export const SourceLink = styled(Link)` max-width: 100%; `; diff --git a/src/components/Errors/NewErrorCard/types.ts b/src/components/Errors/NewErrorCard/types.ts index 957a535e7..b532b2421 100644 --- a/src/components/Errors/NewErrorCard/types.ts +++ b/src/components/Errors/NewErrorCard/types.ts @@ -3,6 +3,7 @@ import { GlobalErrorData } from "../GlobalErrorsList/types"; export interface NewErrorCardProps { data: GlobalErrorData; onSourceLinkClick: (codeObjectId: string) => void; + onPinChange: () => void; } export interface ContainerProps { @@ -15,12 +16,7 @@ export interface OccurrenceChartContainerProps { $transitionClassName: string; } -export interface PinErrorPayload { - id: string; - environment: string; -} - -export interface UnpinErrorPayload { +export interface PinUnpinErrorPayload { id: string; environment: string; } From 9defc0698059d8ae9dafab126e6c66cb46dc9229 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Wed, 23 Oct 2024 16:28:53 +0200 Subject: [PATCH 06/11] Improve test --- src/hooks/useFetchData.test.ts | 86 ++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/src/hooks/useFetchData.test.ts b/src/hooks/useFetchData.test.ts index 14f6c9da5..37836c61e 100644 --- a/src/hooks/useFetchData.test.ts +++ b/src/hooks/useFetchData.test.ts @@ -142,47 +142,51 @@ describe("useFetchData", () => { expect(mockSendMessageToDigma).toHaveBeenCalledTimes(2); }); - it("should fetch data on payload change with interval", () => { - try { - jest.useFakeTimers(); - const { rerender } = setup({ - refreshOnPayloadChange: true, - refreshWithInterval: true, - refreshInterval: 500 - }); - - expect(mockSendMessageToDigma).toHaveBeenCalledTimes(1); - const newPayload = { key: "newValue" }; - rerender({ - config: { - requestAction, - responseAction, - refreshOnPayloadChange: true, - refreshWithInterval: true, - refreshInterval: 500 - }, - payload: newPayload - }); - - expect(mockSendMessageToDigma).toHaveBeenCalledTimes(2); - const handleData = (dispatcher.addActionListener as jest.Mock).mock - .calls[0][1] as ActionListener; // eslint-disable-line @typescript-eslint/no-unsafe-member-access - - act(() => { - handleData({ data: "testData" }, Date.now()); - }); - - act(() => { - jest.advanceTimersByTime(5000); - }); - - expect(mockSendMessageToDigma).toHaveBeenNthCalledWith(3, { - action: requestAction, - payload: newPayload - }); - } finally { - jest.useRealTimers(); - } + it("should fetch data with the new payload on payload change with interval", () => { + jest.useFakeTimers(); + + const refreshInterval = 500; + + const config = { + refreshOnPayloadChange: true, + refreshWithInterval: true, + refreshInterval + }; + + const { rerender } = setup(config); + + expect(mockSendMessageToDigma).toHaveBeenCalledTimes(1); + + const newPayload = { key: "newValue" }; + + rerender({ + config: { + requestAction, + responseAction, + ...config + }, + payload: newPayload + }); + + expect(mockSendMessageToDigma).toHaveBeenCalledTimes(2); + + const handleData = (dispatcher.addActionListener as jest.Mock).mock + .calls[0][1] as ActionListener; // eslint-disable-line @typescript-eslint/no-unsafe-member-access + + act(() => { + handleData({ data: "testData" }, Date.now()); + }); + + act(() => { + jest.advanceTimersByTime(refreshInterval); + }); + + expect(mockSendMessageToDigma).toHaveBeenNthCalledWith(3, { + action: requestAction, + payload: newPayload + }); + + jest.useRealTimers(); }); it("should fetch data on refreshWithInterval change", () => { From 2e2ef92f5c6ec1545b10265068a7f4037654879b Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Wed, 23 Oct 2024 18:26:51 +0200 Subject: [PATCH 07/11] Add custom hook for pin/unpin operations --- .../Errors/GlobalErrorsList/index.tsx | 12 ++- .../Errors/GlobalErrorsList/usePinning.ts | 86 ++++++++++++++++++ src/components/Errors/NewErrorCard/index.tsx | 51 ++++++----- src/components/Errors/NewErrorCard/types.ts | 3 +- .../common/InsightCard/hooks/useDismissal.ts | 89 ++++++++++--------- .../insightCards/common/InsightCard/types.ts | 8 +- src/components/Insights/types.ts | 6 +- src/components/common/FilterPopup/styles.ts | 3 +- 8 files changed, 177 insertions(+), 81 deletions(-) create mode 100644 src/components/Errors/GlobalErrorsList/usePinning.ts diff --git a/src/components/Errors/GlobalErrorsList/index.tsx b/src/components/Errors/GlobalErrorsList/index.tsx index 06747bfab..9c9da253b 100644 --- a/src/components/Errors/GlobalErrorsList/index.tsx +++ b/src/components/Errors/GlobalErrorsList/index.tsx @@ -214,7 +214,7 @@ export const GlobalErrorsList = () => { } }, [data, setGlobalErrorsData]); - // Disable animations after pin/unpin + // Disable animations after pin/unpin actions useEffect(() => { if (!previousList || !list || !latestPinChangedId) { return; @@ -312,10 +312,15 @@ export const GlobalErrorsList = () => { setGlobalErrorsViewMode(ViewMode.All); }; - const handlePinChange = () => { + const handlePinStatusToggle = () => { toggleAnimations(true); }; + const handlePinStatusChange = (errorId: string) => { + setLatestPinChangedId(errorId); + getData(); + }; + const areAnyFiltersApplied = search || [ @@ -381,7 +386,8 @@ export const GlobalErrorsList = () => { key={x.id} data={x} onSourceLinkClick={handleErrorSourceLinkClick} - onPinChange={handlePinChange} + onPinStatusChange={handlePinStatusChange} + onPinStatusToggle={handlePinStatusToggle} /> ))} diff --git a/src/components/Errors/GlobalErrorsList/usePinning.ts b/src/components/Errors/GlobalErrorsList/usePinning.ts new file mode 100644 index 000000000..2c240b6e0 --- /dev/null +++ b/src/components/Errors/GlobalErrorsList/usePinning.ts @@ -0,0 +1,86 @@ +import { useCallback, useEffect, useState } from "react"; +import { dispatcher } from "../../../dispatcher"; +import { useConfigSelector } from "../../../store/config/useConfigSelector"; +import { actions } from "../actions"; +import { PinUnpinErrorPayload } from "../NewErrorCard/types"; +import { SetPinUnpinErrorResultPayload } from "./types"; + +export const usePinning = (errorId: string) => { + const { environment } = useConfigSelector(); + const [data, setData] = useState<{ + action: string; + payload: SetPinUnpinErrorResultPayload; + } | null>(null); + const [isOperationInProgress, setIsOperationInProgress] = useState(false); + + useEffect(() => { + const handlePinErrorResult = (payload: unknown) => { + handleResult(actions.PIN_ERROR, payload); + }; + + const handleUnpinErrorResult = (payload: unknown) => { + handleResult(actions.UNPIN_ERROR, payload); + }; + + const handleResult = (action: string, data: unknown) => { + const payload = data as SetPinUnpinErrorResultPayload; + if (errorId === payload.id) { + setData({ action, payload }); + setIsOperationInProgress(false); + } + }; + + dispatcher.addActionListener( + actions.SET_PIN_ERROR_RESULT, + handlePinErrorResult + ); + dispatcher.addActionListener( + actions.SET_UNPIN_ERROR_RESULT, + handleUnpinErrorResult + ); + + return () => { + dispatcher.removeActionListener( + actions.SET_PIN_ERROR_RESULT, + handlePinErrorResult + ); + dispatcher.removeActionListener( + actions.SET_UNPIN_ERROR_RESULT, + handleUnpinErrorResult + ); + }; + }, [errorId]); + + const sendAction = useCallback( + (action: string) => { + if (!environment) { + return; + } + + window.sendMessageToDigma({ + action, + payload: { + id: errorId, + environment: environment?.id + } + }); + setIsOperationInProgress(true); + }, + [errorId, environment] + ); + + const pin = useCallback(() => { + sendAction(actions.PIN_ERROR); + }, [sendAction]); + + const unpin = useCallback(() => { + sendAction(actions.UNPIN_ERROR); + }, [sendAction]); + + return { + pin, + unpin, + data, + isInProgress: isOperationInProgress + }; +}; diff --git a/src/components/Errors/NewErrorCard/index.tsx b/src/components/Errors/NewErrorCard/index.tsx index d95631d31..f9d065117 100644 --- a/src/components/Errors/NewErrorCard/index.tsx +++ b/src/components/Errors/NewErrorCard/index.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { CSSTransition } from "react-transition-group"; import { getFeatureFlagValue } from "../../../featureFlags"; +import { usePrevious } from "../../../hooks/usePrevious"; import { useConfigSelector } from "../../../store/config/useConfigSelector"; import { FeatureFlag } from "../../../types"; import { changeScope } from "../../../utils/actions/changeScope"; @@ -16,14 +17,14 @@ import { PinFillIcon } from "../../common/icons/16px/PinFillIcon"; import { PinIcon } from "../../common/icons/16px/PinIcon"; import { NewIconButton } from "../../common/v3/NewIconButton"; import { Tooltip } from "../../common/v3/Tooltip"; -import { actions } from "../actions"; import { TimestampKeyValue } from "../ErrorCard/TimestampKeyValue"; +import { usePinning } from "../GlobalErrorsList/usePinning"; import { getTagType, HIGH_SEVERITY_SCORE_THRESHOLD } from "../Score"; import { trackingEvents } from "../tracking"; import { useDismissal } from "./hooks/useDismissal"; import { OccurrenceChart } from "./OccurrenceChart"; import * as s from "./styles"; -import { NewErrorCardProps, PinUnpinErrorPayload } from "./types"; +import { NewErrorCardProps } from "./types"; export const getStatusString = (status: string) => status.toLowerCase().startsWith("recent") ? "Recent" : status; @@ -31,11 +32,12 @@ export const getStatusString = (status: string) => export const NewErrorCard = ({ data, onSourceLinkClick, - onPinChange + onPinStatusChange, + onPinStatusToggle }: NewErrorCardProps) => { const [isHistogramVisible, setIsHistogramVisible] = useState(false); const chartContainerRef = useRef(null); - const { environment, backendInfo } = useConfigSelector(); + const { backendInfo } = useConfigSelector(); const isOccurrenceChartEnabled = getFeatureFlagValue( backendInfo, @@ -52,6 +54,14 @@ export const NewErrorCard = ({ FeatureFlag.IS_GLOBAL_ERROR_DISMISS_ENABLED ); + const { + pin, + unpin, + data: pinUnpinResponse, + isInProgress: isPinUnpinInProgress + } = usePinning(data.id); + const previousPinUnpinResponse = usePrevious(pinUnpinResponse); + const { id, affectedEndpoints, @@ -90,6 +100,15 @@ export const NewErrorCard = ({ } }, [selectorOptions, selectedEndpoint]); + useEffect(() => { + if ( + previousPinUnpinResponse !== pinUnpinResponse && + pinUnpinResponse?.payload.status === "success" + ) { + onPinStatusChange(pinUnpinResponse.payload.id); + } + }, [onPinStatusChange, pinUnpinResponse, previousPinUnpinResponse]); + const handleLinkClick = () => { sendUserActionTrackingEvent(trackingEvents.ERROR_CARD_SOURCE_LINK_CLICKED); onSourceLinkClick(id); @@ -139,28 +158,13 @@ export const NewErrorCard = ({ } ); - if (!environment) { - return; - } - if (value) { - window.sendMessageToDigma({ - action: actions.PIN_ERROR, - payload: { - id, - environment: environment.id - } - }); + pin(); } else { - window.sendMessageToDigma({ - action: actions.UNPIN_ERROR, - payload: { - id, - environment: environment.id - } - }); + unpin(); } - onPinChange(); + + onPinStatusToggle(); }; const handleDismissalButtonClick = () => { @@ -193,6 +197,7 @@ export const NewErrorCard = ({ buttonType={"secondaryBorderless"} icon={isPinned ? PinFillIcon : PinIcon} onClick={handlePinUnpinButtonClick} + isDisabled={isPinUnpinInProgress} /> ] : []), diff --git a/src/components/Errors/NewErrorCard/types.ts b/src/components/Errors/NewErrorCard/types.ts index b532b2421..292d960a7 100644 --- a/src/components/Errors/NewErrorCard/types.ts +++ b/src/components/Errors/NewErrorCard/types.ts @@ -3,7 +3,8 @@ import { GlobalErrorData } from "../GlobalErrorsList/types"; export interface NewErrorCardProps { data: GlobalErrorData; onSourceLinkClick: (codeObjectId: string) => void; - onPinChange: () => void; + onPinStatusChange: (errorId: string) => void; + onPinStatusToggle: () => void; } export interface ContainerProps { diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts index 8914ce77d..17531b3ff 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts +++ b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts @@ -1,72 +1,79 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { dispatcher } from "../../../../../../../../dispatcher"; import { actions } from "../../../../../../actions"; -import { - DismissInsightPayload, - UndismissInsightPayload -} from "../../../../../../types"; -import { DismissResponsePayload, UndismissResponsePayload } from "../types"; +import { DismissUndismissInsightPayload } from "../../../../../../types"; +import { DismissUndismissResponsePayload } from "../types"; export const useDismissal = (insightId: string) => { - const [isDismissalChangeInProgress, setIsDismissalChangeInProgress] = - useState(false); + const [data, setData] = useState<{ + action: string; + payload: DismissUndismissResponsePayload; + } | null>(null); + const [isOperationInProgress, setIsOperationInProgress] = useState(false); useEffect(() => { - const handleDismissed = (data: unknown) => { - if (insightId === (data as DismissResponsePayload).insightId) { - setIsDismissalChangeInProgress(false); - } + const handleDismissResponse = (payload: unknown) => { + handleResponse(actions.SET_DISMISS_RESPONSE, payload); }; - dispatcher.addActionListener(actions.SET_DISMISS_RESPONSE, handleDismissed); - - return () => { - dispatcher.removeActionListener( - actions.SET_DISMISS_RESPONSE, - handleDismissed - ); + const handleUndismissResponse = (payload: unknown) => { + handleResponse(actions.SET_UNDISMISS_RESPONSE, payload); }; - }, []); - useEffect(() => { - const handleUndismissed = (data: unknown) => { - if (insightId === (data as UndismissResponsePayload).insightId) { - setIsDismissalChangeInProgress(false); + const handleResponse = (action: string, data: unknown) => { + const payload = data as DismissUndismissResponsePayload; + if (insightId === payload.insightId) { + setData({ action, payload }); + setIsOperationInProgress(false); } }; + dispatcher.addActionListener( + actions.SET_DISMISS_RESPONSE, + handleDismissResponse + ); dispatcher.addActionListener( actions.SET_UNDISMISS_RESPONSE, - handleUndismissed + handleUndismissResponse ); return () => { + dispatcher.removeActionListener( + actions.SET_DISMISS_RESPONSE, + handleDismissResponse + ); dispatcher.removeActionListener( actions.SET_UNDISMISS_RESPONSE, - handleUndismissed + handleUndismissResponse ); }; }, [insightId]); - return { - isDismissalChangeInProgress, - dismiss: () => { - window.sendMessageToDigma({ - action: actions.DISMISS, + const sendAction = useCallback( + (action: string) => { + window.sendMessageToDigma({ + action, payload: { insightId } }); - setIsDismissalChangeInProgress(true); + setIsOperationInProgress(true); }, - show: () => { - window.sendMessageToDigma({ - action: actions.UNDISMISS, - payload: { - insightId: insightId - } - }); - setIsDismissalChangeInProgress(true); - } + [insightId] + ); + + const dismiss = useCallback(() => { + sendAction(actions.DISMISS); + }, [sendAction]); + + const undismiss = useCallback(() => { + sendAction(actions.UNDISMISS); + }, [sendAction]); + + return { + dismiss, + show: undismiss, + data, + isDismissalChangeInProgress: isOperationInProgress }; }; diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts index a756601ca..46285d38f 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts +++ b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts @@ -41,13 +41,7 @@ export interface StyledCardProps extends CardProps { $isReadable?: boolean; } -export interface DismissResponsePayload { - insightId: string; - status: "success" | "failure"; - error?: string; -} - -export interface UndismissResponsePayload { +export interface DismissUndismissResponsePayload { insightId: string; status: "success" | "failure"; error?: string; diff --git a/src/components/Insights/types.ts b/src/components/Insights/types.ts index afd752757..4b22294f8 100644 --- a/src/components/Insights/types.ts +++ b/src/components/Insights/types.ts @@ -679,10 +679,6 @@ export interface ScopedInsightsQuery extends InsightsQuery { export { InsightType }; -export interface DismissInsightPayload { - insightId: string; -} - -export interface UndismissInsightPayload { +export interface DismissUndismissInsightPayload { insightId: string; } diff --git a/src/components/common/FilterPopup/styles.ts b/src/components/common/FilterPopup/styles.ts index fc068f71c..9380f81f8 100644 --- a/src/components/common/FilterPopup/styles.ts +++ b/src/components/common/FilterPopup/styles.ts @@ -15,6 +15,7 @@ export const Container = styled.div` box-shadow: 0 2px 4px 0 rgb(0 0 0 / 29%); font-size: 14px; color: ${({ theme }) => theme.colors.v3.text.tertiary}; + margin: 0 8px; `; export const Header = styled.div` @@ -28,7 +29,7 @@ export const Header = styled.div` export const Footer = styled.div` padding: 8px 0; display: flex; - justify-content: space-between; + gap: 8px; align-items: center; `; From 84cd1bdf3e081abe8eae50bab4ac3f17889f0cd0 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Thu, 24 Oct 2024 21:59:26 +0300 Subject: [PATCH 08/11] Updated styled for dismiss --- .../NewErrorCard/NewErrorCard.stories.tsx | 3 + src/components/Errors/NewErrorCard/index.tsx | 179 +++++++++--------- src/components/Errors/NewErrorCard/styles.ts | 30 ++- .../insightCards/common/InsightCard/styles.ts | 22 +-- src/components/common/DismissPanel/index.tsx | 5 +- src/components/common/DismissPanel/types.ts | 1 + 6 files changed, 127 insertions(+), 113 deletions(-) diff --git a/src/components/Errors/NewErrorCard/NewErrorCard.stories.tsx b/src/components/Errors/NewErrorCard/NewErrorCard.stories.tsx index 50f57f39f..e4180cc04 100644 --- a/src/components/Errors/NewErrorCard/NewErrorCard.stories.tsx +++ b/src/components/Errors/NewErrorCard/NewErrorCard.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; import { NewErrorCard } from "."; import { mockedGlobalErrorData } from "./mockData"; @@ -20,12 +21,14 @@ type Story = StoryObj; export const Default: Story = { args: { + onPinStatusChange: fn(), data: mockedGlobalErrorData } }; export const Critical: Story = { args: { + onPinStatusChange: fn(), data: { ...mockedGlobalErrorData, score: { diff --git a/src/components/Errors/NewErrorCard/index.tsx b/src/components/Errors/NewErrorCard/index.tsx index f9d065117..f81917c39 100644 --- a/src/components/Errors/NewErrorCard/index.tsx +++ b/src/components/Errors/NewErrorCard/index.tsx @@ -11,7 +11,6 @@ import { getEndpointKey } from "../../common/AffectedEndpointsSelector"; import { Option } from "../../common/AffectedEndpointsSelector/types"; -import { DismissPanel } from "../../common/DismissPanel"; import { HistogramIcon } from "../../common/icons/16px/HistogramIcon"; import { PinFillIcon } from "../../common/icons/16px/PinFillIcon"; import { PinIcon } from "../../common/icons/16px/PinIcon"; @@ -216,96 +215,100 @@ export const NewErrorCard = ({ return ( - - - - {errorType} - - - - {fromDisplayName} - - - - {status && ( - - - - - Score:{" "} - {score.score} - - - } - type={statusTagType} - /> - )} - - {selectorOptions.length > 0 && ( - - Affected Endpoints ({selectorOptions.length}) - - - )} - {isOccurrenceChartEnabled && selectedEndpoint && ( - <> - - - - - - - )} - {toolbarActions.length > 0 && ( - - {isDismissEnabled && ( - + + + + {errorType} + + + + {fromDisplayName} + + + + {status && ( + + + + + Score:{" "} + {score.score} + + } + type={statusTagType} /> )} - {toolbarActions} - - )} + + {selectorOptions.length > 0 && ( + + Affected Endpoints ({selectorOptions.length}) + + + )} + {isOccurrenceChartEnabled && selectedEndpoint && ( + <> + + + + + + + )} + + + {(toolbarActions.length > 0 || isDismissEnabled) && ( + + {isDismissEnabled && ( + + )} + {toolbarActions} + + )} + ); }; diff --git a/src/components/Errors/NewErrorCard/styles.ts b/src/components/Errors/NewErrorCard/styles.ts index bdb2a8060..995eb039f 100644 --- a/src/components/Errors/NewErrorCard/styles.ts +++ b/src/components/Errors/NewErrorCard/styles.ts @@ -3,6 +3,8 @@ import { bodySemiboldTypography, footnoteRegularTypography } from "../../common/App/typographies"; +import { DismissPanel } from "../../common/DismissPanel"; +import { DismissDialog } from "../../common/DismissPanel/styles"; import { Link } from "../../common/v3/Link"; import { Tag } from "../../common/v3/Tag"; import { HEIGHT } from "./OccurrenceChart/styles"; @@ -16,7 +18,6 @@ export const Container = styled.div` flex-direction: column; gap: 16px; border-radius: 4px; - padding: 12px; border: 1px solid ${({ theme, $isPinned, $isCritical }) => $isPinned @@ -112,8 +113,33 @@ export const OccurrenceChartContainer = styled.div` : ""} `; -export const DismissDialog = styled.div` - display: flex; - justify-content: space-between; - padding: 7px; - align-items: center; - margin: -7px; - background: ${({ theme }) => theme.colors.v3.surface.sidePanelHeader}; - - ${subscriptRegularTypography} -`; - -export const DismissDialogActions = styled.div` - display: flex; - gap: 8px; -`; - -export const ButtonContainer = styled.div` - display: flex; - align-items: center; -`; - export const InfoActionButton = styled(NewIconButton)` &:hover { color: ${({ theme }) => theme.colors.v3.status.medium}; diff --git a/src/components/common/DismissPanel/index.tsx b/src/components/common/DismissPanel/index.tsx index f3af46059..64b411a47 100644 --- a/src/components/common/DismissPanel/index.tsx +++ b/src/components/common/DismissPanel/index.tsx @@ -10,7 +10,8 @@ const DismissPanelComponent = ({ state, onShow, onDismiss, - confirmationMessage + confirmationMessage, + className }: DismissPanelProps) => { const [isDismissConfirmationOpened, setDismissConfirmationOpened] = useState(false); @@ -32,7 +33,7 @@ const DismissPanelComponent = ({ return ( <> {!isDismissConfirmationOpened ? ( - + {state === "dismissed" && ( void; onDismiss: () => void; confirmationMessage: string; + className?: string; } From 6d1ff4954c143f378060bdf969343e55fe410df0 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Fri, 25 Oct 2024 16:12:37 +0300 Subject: [PATCH 09/11] Added tests --- .../Errors/NewErrorCard/hooks/useDismissal.ts | 77 ++++------------ src/components/Errors/NewErrorCard/styles.ts | 5 -- .../common/InsightCard/hooks/useDismissal.ts | 82 ++++------------- .../insightCards/common/InsightCard/types.ts | 3 +- src/components/Insights/types.ts | 9 +- src/components/common/DismissPanel/styles.ts | 2 + src/hooks/useAction.test.ts | 87 +++++++++++++++++++ src/hooks/useAction.ts | 56 ++++++++++++ src/types.ts | 4 + 9 files changed, 193 insertions(+), 132 deletions(-) create mode 100644 src/hooks/useAction.test.ts create mode 100644 src/hooks/useAction.ts diff --git a/src/components/Errors/NewErrorCard/hooks/useDismissal.ts b/src/components/Errors/NewErrorCard/hooks/useDismissal.ts index 6ec2a7a00..b2c1532b5 100644 --- a/src/components/Errors/NewErrorCard/hooks/useDismissal.ts +++ b/src/components/Errors/NewErrorCard/hooks/useDismissal.ts @@ -1,71 +1,30 @@ -import { useEffect, useState } from "react"; -import { dispatcher } from "../../../../dispatcher"; +import { useAction } from "../../../../hooks/useAction"; +import { Identifier } from "../../../../types"; import { actions } from "../../actions"; -import { DismissPayload, UndismissPayload } from "./types"; +import { DismissPayload } from "./types"; export const useDismissal = (id: string) => { - const [isDismissalChangeInProgress, setIsDismissalChangeInProgress] = - useState(false); - - useEffect(() => { - const handleDismissed = (data: unknown) => { - if (id === (data as DismissPayload).id) { - setIsDismissalChangeInProgress(false); - } - }; - - dispatcher.addActionListener( + const { isOperationInProgress: isDismissInProgress, execute: dismiss } = + useAction( + actions.DISMISS_ERROR, actions.SET_DISMISS_ERROR_RESULT, - handleDismissed - ); - - return () => { - dispatcher.removeActionListener( - actions.SET_DISMISS_ERROR_RESULT, - handleDismissed - ); - }; - }, [id]); - - useEffect(() => { - const handleUndismissed = (data: unknown) => { - if (id === (data as UndismissPayload).id) { - setIsDismissalChangeInProgress(false); + { + id } - }; + ); - dispatcher.addActionListener( + const { isOperationInProgress: isUndismissInProgress, execute: undismiss } = + useAction( + actions.UNDISMISS_ERROR, actions.SET_UNDISMISS_ERROR_RESULT, - handleUndismissed + { + id + } ); - return () => { - dispatcher.removeActionListener( - actions.SET_UNDISMISS_ERROR_RESULT, - handleUndismissed - ); - }; - }, [id]); - return { - isDismissalChangeInProgress, - dismiss: () => { - window.sendMessageToDigma({ - action: actions.DISMISS_ERROR, - payload: { - id - } - }); - setIsDismissalChangeInProgress(true); - }, - show: () => { - window.sendMessageToDigma({ - action: actions.UNDISMISS_ERROR, - payload: { - id - } - }); - setIsDismissalChangeInProgress(true); - } + dismiss, + show: undismiss, + isDismissalChangeInProgress: isDismissInProgress || isUndismissInProgress }; }; diff --git a/src/components/Errors/NewErrorCard/styles.ts b/src/components/Errors/NewErrorCard/styles.ts index 995eb039f..54e24321a 100644 --- a/src/components/Errors/NewErrorCard/styles.ts +++ b/src/components/Errors/NewErrorCard/styles.ts @@ -4,7 +4,6 @@ import { footnoteRegularTypography } from "../../common/App/typographies"; import { DismissPanel } from "../../common/DismissPanel"; -import { DismissDialog } from "../../common/DismissPanel/styles"; import { Link } from "../../common/v3/Link"; import { Tag } from "../../common/v3/Tag"; import { HEIGHT } from "./OccurrenceChart/styles"; @@ -131,10 +130,6 @@ export const StyledDismissPanel = styled(DismissPanel)` display: flex; flex: 1; width: 100%; - - & ${DismissDialog} { - margin: 10px; - } `; export const Content = styled.div` diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts index 17531b3ff..4d1057508 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts +++ b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts @@ -1,79 +1,31 @@ -import { useCallback, useEffect, useState } from "react"; -import { dispatcher } from "../../../../../../../../dispatcher"; +import { useAction } from "../../../../../../../../hooks/useAction"; import { actions } from "../../../../../../actions"; import { DismissUndismissInsightPayload } from "../../../../../../types"; import { DismissUndismissResponsePayload } from "../types"; export const useDismissal = (insightId: string) => { - const [data, setData] = useState<{ - action: string; - payload: DismissUndismissResponsePayload; - } | null>(null); - const [isOperationInProgress, setIsOperationInProgress] = useState(false); - - useEffect(() => { - const handleDismissResponse = (payload: unknown) => { - handleResponse(actions.SET_DISMISS_RESPONSE, payload); - }; - - const handleUndismissResponse = (payload: unknown) => { - handleResponse(actions.SET_UNDISMISS_RESPONSE, payload); - }; - - const handleResponse = (action: string, data: unknown) => { - const payload = data as DismissUndismissResponsePayload; - if (insightId === payload.insightId) { - setData({ action, payload }); - setIsOperationInProgress(false); - } - }; - - dispatcher.addActionListener( + const { isOperationInProgress: isDismissInProgress, execute: dismiss } = + useAction( + actions.DISMISS, actions.SET_DISMISS_RESPONSE, - handleDismissResponse + { + id: insightId, + insightId + } ); - dispatcher.addActionListener( + + const { isOperationInProgress: isUndismissInProgress, execute: undismiss } = + useAction( + actions.UNDISMISS, actions.SET_UNDISMISS_RESPONSE, - handleUndismissResponse + { + id: insightId, + insightId + } ); - - return () => { - dispatcher.removeActionListener( - actions.SET_DISMISS_RESPONSE, - handleDismissResponse - ); - dispatcher.removeActionListener( - actions.SET_UNDISMISS_RESPONSE, - handleUndismissResponse - ); - }; - }, [insightId]); - - const sendAction = useCallback( - (action: string) => { - window.sendMessageToDigma({ - action, - payload: { - insightId - } - }); - setIsOperationInProgress(true); - }, - [insightId] - ); - - const dismiss = useCallback(() => { - sendAction(actions.DISMISS); - }, [sendAction]); - - const undismiss = useCallback(() => { - sendAction(actions.UNDISMISS); - }, [sendAction]); - return { dismiss, show: undismiss, - data, - isDismissalChangeInProgress: isOperationInProgress + isDismissalChangeInProgress: isDismissInProgress || isUndismissInProgress }; }; diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts index 46285d38f..4c96fc031 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts +++ b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts @@ -1,4 +1,5 @@ import { ReactNode } from "react"; +import { Identifier } from "../../../../../../../types"; import { ScopeSpan } from "../../../../../../common/App/types"; import { CardProps } from "../../../../../../common/v3/Card/types"; import { GenericCodeObjectInsight, InsightType } from "../../../../../types"; @@ -41,7 +42,7 @@ export interface StyledCardProps extends CardProps { $isReadable?: boolean; } -export interface DismissUndismissResponsePayload { +export interface DismissUndismissResponsePayload extends Identifier { insightId: string; status: "success" | "failure"; error?: string; diff --git a/src/components/Insights/types.ts b/src/components/Insights/types.ts index 4b22294f8..93d18d06e 100644 --- a/src/components/Insights/types.ts +++ b/src/components/Insights/types.ts @@ -1,6 +1,11 @@ import { MemoExoticComponent } from "react"; import { Duration } from "../../globals"; -import { InsightType, SpanInfo, SpanInstanceInfo } from "../../types"; +import { + Identifier, + InsightType, + SpanInfo, + SpanInstanceInfo +} from "../../types"; import { Sorting } from "../common/SortingSelector/types"; import { IconProps } from "../common/icons/types"; import { InsightFilterType } from "./InsightsCatalog/types"; @@ -679,6 +684,6 @@ export interface ScopedInsightsQuery extends InsightsQuery { export { InsightType }; -export interface DismissUndismissInsightPayload { +export interface DismissUndismissInsightPayload extends Identifier { insightId: string; } diff --git a/src/components/common/DismissPanel/styles.ts b/src/components/common/DismissPanel/styles.ts index 46ba7d084..5e1fc3a49 100644 --- a/src/components/common/DismissPanel/styles.ts +++ b/src/components/common/DismissPanel/styles.ts @@ -10,6 +10,8 @@ export const DismissDialog = styled.div` background: ${({ theme }) => theme.colors.v3.surface.sidePanelHeader}; position: absolute; width: 100%; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; ${subscriptRegularTypography} `; diff --git a/src/hooks/useAction.test.ts b/src/hooks/useAction.test.ts new file mode 100644 index 000000000..ac32f3ccf --- /dev/null +++ b/src/hooks/useAction.test.ts @@ -0,0 +1,87 @@ +import { act, renderHook } from "@testing-library/react"; +import { ActionListener } from "../api/types"; +import { dispatcher } from "../dispatcher"; +import { useAction } from "./useAction"; + +jest.mock("../dispatcher", () => ({ + dispatcher: { + addActionListener: jest.fn(), + removeActionListener: jest.fn() + } +})); + +const mockSendMessageToDigma = jest.fn(); +window.sendMessageToDigma = mockSendMessageToDigma; + +describe("useAction Hook", () => { + const requestAction = "testRequestAction"; + const responseAction = "testResponseAction"; + const mockPayload = { id: "123" }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should initialize with no data and operation not in progress", () => { + const { result } = renderHook(() => + useAction(requestAction, responseAction, mockPayload) + ); + expect(result.current.data).toBeNull(); + expect(result.current.isOperationInProgress).toBe(false); + }); + + it("should add and remove action listeners on mount and unmount", () => { + const { unmount } = renderHook(() => + useAction(requestAction, responseAction, mockPayload) + ); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(dispatcher.addActionListener).toHaveBeenCalledWith( + responseAction, + expect.any(Function) + ); + + unmount(); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(dispatcher.removeActionListener).toHaveBeenCalledWith( + responseAction, + expect.any(Function) + ); + }); + + it("should set data and stop operation on valid action response", () => { + const { result } = renderHook(() => + useAction(requestAction, responseAction, mockPayload) + ); + + act(() => { + const handleActionResponse = (dispatcher.addActionListener as jest.Mock) + .mock.calls[0][1] as ActionListener; // eslint-disable-line @typescript-eslint/no-unsafe-member-access + + handleActionResponse({ id: "123" }, Date.now()); + }); + + expect(result.current.data).toEqual({ + action: responseAction, + payload: mockPayload + }); + expect(result.current.isOperationInProgress).toBe(false); + }); + + it("should start operation in progress and send message on execute", () => { + const { result } = renderHook(() => + useAction(requestAction, responseAction, mockPayload) + ); + + act(() => { + result.current.execute(); + }); + + expect(result.current.isOperationInProgress).toBe(true); + expect(window.sendMessageToDigma).toHaveBeenCalledWith({ + action: requestAction, + payload: mockPayload + }); + }); +}); diff --git a/src/hooks/useAction.ts b/src/hooks/useAction.ts new file mode 100644 index 000000000..75fbfbc86 --- /dev/null +++ b/src/hooks/useAction.ts @@ -0,0 +1,56 @@ +import { useCallback, useEffect, useState } from "react"; +import { dispatcher } from "../dispatcher"; +import { Identifier } from "../types"; + +export const useAction = < + TPayload extends Identifier, + TResponse extends Identifier +>( + requestAction: string, + responseAction: string, + payload: TPayload +) => { + const [data, setData] = useState<{ + action: string; + payload: TResponse; + } | null>(null); + const [isOperationInProgress, setIsOperationInProgress] = useState(false); + + useEffect(() => { + const handleActionResponse = (data: unknown) => { + const result = data as TResponse; + + if (result.id === payload.id) { + setData({ action: responseAction, payload: result }); + setIsOperationInProgress(false); + } + }; + + dispatcher.addActionListener(responseAction, handleActionResponse); + + return () => { + dispatcher.removeActionListener(responseAction, handleActionResponse); + }; + }, [payload, payload.id, responseAction]); + + const sendAction = useCallback( + (action: string) => { + window.sendMessageToDigma({ + action, + payload: payload + }); + setIsOperationInProgress(true); + }, + [payload] + ); + + const execute = useCallback(() => { + sendAction(requestAction); + }, [requestAction, sendAction]); + + return { + execute, + data, + isOperationInProgress + }; +}; diff --git a/src/types.ts b/src/types.ts index 49c5f9b0c..baf4518f6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -136,3 +136,7 @@ export interface SendPluginEventPayload { name: string; payload?: Record; } + +export interface Identifier { + id: string; +} From 7d70724f78f39429c7aa0214e07988dd9a7b8652 Mon Sep 17 00:00:00 2001 From: opoliarush Date: Fri, 25 Oct 2024 16:26:41 +0300 Subject: [PATCH 10/11] Fix bug --- .../common/InsightCard/hooks/useDismissal.ts | 8 +++++-- src/hooks/useAction.test.ts | 24 +++++++++++++++++++ src/hooks/useAction.ts | 9 +++---- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts index 4d1057508..d9edc0fd9 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts +++ b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/hooks/useDismissal.ts @@ -3,6 +3,8 @@ import { actions } from "../../../../../../actions"; import { DismissUndismissInsightPayload } from "../../../../../../types"; import { DismissUndismissResponsePayload } from "../types"; +const mapId = (response: DismissUndismissResponsePayload) => response.insightId; + export const useDismissal = (insightId: string) => { const { isOperationInProgress: isDismissInProgress, execute: dismiss } = useAction( @@ -11,7 +13,8 @@ export const useDismissal = (insightId: string) => { { id: insightId, insightId - } + }, + mapId ); const { isOperationInProgress: isUndismissInProgress, execute: undismiss } = @@ -21,7 +24,8 @@ export const useDismissal = (insightId: string) => { { id: insightId, insightId - } + }, + mapId ); return { dismiss, diff --git a/src/hooks/useAction.test.ts b/src/hooks/useAction.test.ts index ac32f3ccf..7c12f3005 100644 --- a/src/hooks/useAction.test.ts +++ b/src/hooks/useAction.test.ts @@ -69,6 +69,30 @@ describe("useAction Hook", () => { expect(result.current.isOperationInProgress).toBe(false); }); + it("should set data and stop operation on valid action response with customId", () => { + const { result } = renderHook(() => + useAction( + requestAction, + responseAction, + mockPayload, + (result: { errorId: string; id: string }) => result.errorId + ) + ); + + act(() => { + const handleActionResponse = (dispatcher.addActionListener as jest.Mock) + .mock.calls[0][1] as ActionListener; // eslint-disable-line @typescript-eslint/no-unsafe-member-access + + handleActionResponse({ errorId: "123" }, Date.now()); + }); + + expect(result.current.data).toEqual({ + action: responseAction, + payload: { errorId: "123" } + }); + expect(result.current.isOperationInProgress).toBe(false); + }); + it("should start operation in progress and send message on execute", () => { const { result } = renderHook(() => useAction(requestAction, responseAction, mockPayload) diff --git a/src/hooks/useAction.ts b/src/hooks/useAction.ts index 75fbfbc86..89952ce32 100644 --- a/src/hooks/useAction.ts +++ b/src/hooks/useAction.ts @@ -8,7 +8,8 @@ export const useAction = < >( requestAction: string, responseAction: string, - payload: TPayload + payload: TPayload, + mapId?: (response: TResponse) => string ) => { const [data, setData] = useState<{ action: string; @@ -19,8 +20,8 @@ export const useAction = < useEffect(() => { const handleActionResponse = (data: unknown) => { const result = data as TResponse; - - if (result.id === payload.id) { + const responseId = mapId ? mapId(result) : result.id; + if (payload.id === responseId) { setData({ action: responseAction, payload: result }); setIsOperationInProgress(false); } @@ -31,7 +32,7 @@ export const useAction = < return () => { dispatcher.removeActionListener(responseAction, handleActionResponse); }; - }, [payload, payload.id, responseAction]); + }, [mapId, payload, payload.id, responseAction]); const sendAction = useCallback( (action: string) => { From 718b78acb489319a0c63f3f7be8ac57b8d3f5bab Mon Sep 17 00:00:00 2001 From: opoliarush Date: Fri, 25 Oct 2024 22:45:42 +0300 Subject: [PATCH 11/11] fix comments --- .../Errors/GlobalErrorsList/index.tsx | 65 ++++++++++++------- .../Errors/NewErrorCard/hooks/types.ts | 8 +++ .../Errors/NewErrorCard/hooks/useDismissal.ts | 56 +++++++++++----- src/components/Errors/NewErrorCard/index.tsx | 27 ++++++-- src/components/Errors/NewErrorCard/styles.ts | 4 +- src/components/Errors/NewErrorCard/types.ts | 1 + .../insightCards/common/InsightCard/types.ts | 4 +- src/components/Insights/types.ts | 10 +-- src/components/common/icons/16px/EyeIcon.tsx | 4 +- src/hooks/useAction.ts | 5 +- src/types.ts | 4 -- 11 files changed, 124 insertions(+), 64 deletions(-) diff --git a/src/components/Errors/GlobalErrorsList/index.tsx b/src/components/Errors/GlobalErrorsList/index.tsx index f8c6492cd..86265820f 100644 --- a/src/components/Errors/GlobalErrorsList/index.tsx +++ b/src/components/Errors/GlobalErrorsList/index.tsx @@ -17,6 +17,7 @@ import { import { useErrorsSelector } from "../../../store/errors/useErrorsSelector"; import { useStore } from "../../../store/useStore"; import { isNumber } from "../../../typeGuards/isNumber"; +import { isUndefined } from "../../../typeGuards/isUndefined"; import { FeatureFlag } from "../../../types"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; import { formatUnit } from "../../../utils/formatUnit"; @@ -64,6 +65,10 @@ export const GlobalErrorsList = () => { ); const { environment, backendInfo } = useConfigSelector(); + const isDismissEnabled = getFeatureFlagValue( + backendInfo, + FeatureFlag.IS_GLOBAL_ERROR_DISMISS_ENABLED + ); const { globalErrorsViewMode: mode, @@ -164,7 +169,10 @@ export const GlobalErrorsList = () => { >(dataFetcherConfiguration, payload); const isDismissalViewModeButtonVisible = - data?.dismissedCount && data.dismissedCount > 0; // isUndefined - check for backward compatibility, always show when BE does not return this counter + isDismissEnabled && + data && + !isUndefined(data.dismissedCount) && + data.dismissedCount > 0; // Refresh data after pin/unpin actions useEffect(() => { @@ -305,6 +313,10 @@ export const GlobalErrorsList = () => { getData(); }; + const handleDismissalStatusChange = () => { + getData(); + }; + const areAnyFiltersApplied = search || [ @@ -315,6 +327,20 @@ export const GlobalErrorsList = () => { selectedFilters.handlingTypes ].some((x) => x.length > 0); + const renderDismissBtn = () => ( + ( + + )} + onClick={handleDismissalViewModeButtonClick} + /> + ); + return ( {mode === ViewMode.OnlyDismissed && isNumber(data?.dismissedCount) && ( @@ -372,31 +398,10 @@ export const GlobalErrorsList = () => { onSourceLinkClick={handleErrorSourceLinkClick} onPinStatusChange={handlePinStatusChange} onPinStatusToggle={handlePinStatusToggle} + onDismissStatusChange={handleDismissalStatusChange} /> ))} - - {isDismissalViewModeButtonVisible && ( - ( - - )} - onClick={handleDismissalViewModeButtonClick} - /> - )} - ) : areAnyFiltersApplied ? ( { ) : ( )} + {list.length > 0 ? ( + + {isDismissalViewModeButtonVisible && renderDismissBtn()} + + ) : isDismissalViewModeButtonVisible ? ( + renderDismissBtn() + ) : undefined} ) : !environmentId ? ( diff --git a/src/components/Errors/NewErrorCard/hooks/types.ts b/src/components/Errors/NewErrorCard/hooks/types.ts index b46a1d5ec..8b92d0c1a 100644 --- a/src/components/Errors/NewErrorCard/hooks/types.ts +++ b/src/components/Errors/NewErrorCard/hooks/types.ts @@ -1,7 +1,15 @@ export interface DismissPayload { id: string; + status: "success" | "failure"; + error?: { + message: string; + }; } export interface UndismissPayload { id: string; + status: "success" | "failure"; + error?: { + message: string; + }; } diff --git a/src/components/Errors/NewErrorCard/hooks/useDismissal.ts b/src/components/Errors/NewErrorCard/hooks/useDismissal.ts index b2c1532b5..e0be9f0e6 100644 --- a/src/components/Errors/NewErrorCard/hooks/useDismissal.ts +++ b/src/components/Errors/NewErrorCard/hooks/useDismissal.ts @@ -1,30 +1,50 @@ +import { useEffect, useState } from "react"; import { useAction } from "../../../../hooks/useAction"; -import { Identifier } from "../../../../types"; import { actions } from "../../actions"; -import { DismissPayload } from "./types"; +import { DismissPayload, UndismissPayload } from "./types"; export const useDismissal = (id: string) => { - const { isOperationInProgress: isDismissInProgress, execute: dismiss } = - useAction( - actions.DISMISS_ERROR, - actions.SET_DISMISS_ERROR_RESULT, - { - id - } - ); + const [data, setData] = useState<{ + action: string; + payload: DismissPayload | UndismissPayload; + } | null>(null); - const { isOperationInProgress: isUndismissInProgress, execute: undismiss } = - useAction( - actions.UNDISMISS_ERROR, - actions.SET_UNDISMISS_ERROR_RESULT, - { - id - } - ); + const { + isOperationInProgress: isDismissInProgress, + execute: dismiss, + data: dismissData + } = useAction<{ id: string }, DismissPayload>( + actions.DISMISS_ERROR, + actions.SET_DISMISS_ERROR_RESULT, + { + id + } + ); + + const { + isOperationInProgress: isUndismissInProgress, + execute: undismiss, + data: undismissData + } = useAction<{ id: string }, UndismissPayload>( + actions.UNDISMISS_ERROR, + actions.SET_UNDISMISS_ERROR_RESULT, + { + id + } + ); + + useEffect(() => { + setData(undismissData); + }, [undismissData]); + + useEffect(() => { + setData(dismissData); + }, [dismissData]); return { dismiss, show: undismiss, + data, isDismissalChangeInProgress: isDismissInProgress || isUndismissInProgress }; }; diff --git a/src/components/Errors/NewErrorCard/index.tsx b/src/components/Errors/NewErrorCard/index.tsx index f81917c39..1e4bb6e86 100644 --- a/src/components/Errors/NewErrorCard/index.tsx +++ b/src/components/Errors/NewErrorCard/index.tsx @@ -32,6 +32,7 @@ export const NewErrorCard = ({ data, onSourceLinkClick, onPinStatusChange, + onDismissStatusChange, onPinStatusToggle }: NewErrorCardProps) => { const [isHistogramVisible, setIsHistogramVisible] = useState(false); @@ -59,6 +60,7 @@ export const NewErrorCard = ({ data: pinUnpinResponse, isInProgress: isPinUnpinInProgress } = usePinning(data.id); + const previousPinUnpinResponse = usePrevious(pinUnpinResponse); const { @@ -74,7 +76,13 @@ export const NewErrorCard = ({ isDismissed } = data; const statusTagType = getTagType(score.score); - const { isDismissalChangeInProgress, dismiss, show } = useDismissal(id); + const { + isDismissalChangeInProgress, + dismiss, + show, + data: dismissalData + } = useDismissal(id); + const previousDismissalData = usePrevious(dismissalData); const selectorOptions: Option[] = useMemo( () => affectedEndpoints.map((x) => ({ @@ -99,6 +107,15 @@ export const NewErrorCard = ({ } }, [selectorOptions, selectedEndpoint]); + useEffect(() => { + if ( + previousDismissalData !== dismissalData && + dismissalData?.payload.status === "success" + ) { + onDismissStatusChange(dismissalData.payload.id); + } + }, [dismissalData, onDismissStatusChange, previousDismissalData]); + useEffect(() => { if ( previousPinUnpinResponse !== pinUnpinResponse && @@ -288,8 +305,8 @@ export const NewErrorCard = ({ )} - - {(toolbarActions.length > 0 || isDismissEnabled) && ( + {(toolbarActions.length > 0 || isDismissEnabled) && ( + {isDismissEnabled && ( - )} - + + )} ); }; diff --git a/src/components/Errors/NewErrorCard/styles.ts b/src/components/Errors/NewErrorCard/styles.ts index 54e24321a..26c2bb05e 100644 --- a/src/components/Errors/NewErrorCard/styles.ts +++ b/src/components/Errors/NewErrorCard/styles.ts @@ -15,7 +15,6 @@ export const chartContainerTransitionClassName = "chart-container"; export const Container = styled.div` display: flex; flex-direction: column; - gap: 16px; border-radius: 4px; border: 1px solid ${({ theme, $isPinned, $isCritical }) => @@ -113,7 +112,7 @@ export const OccurrenceChartContainer = styled.div void; onPinStatusChange: (errorId: string) => void; + onDismissStatusChange: (errorId: string) => void; onPinStatusToggle: () => void; } diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts index 4c96fc031..993efd77b 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts +++ b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/types.ts @@ -1,5 +1,4 @@ import { ReactNode } from "react"; -import { Identifier } from "../../../../../../../types"; import { ScopeSpan } from "../../../../../../common/App/types"; import { CardProps } from "../../../../../../common/v3/Card/types"; import { GenericCodeObjectInsight, InsightType } from "../../../../../types"; @@ -42,8 +41,9 @@ export interface StyledCardProps extends CardProps { $isReadable?: boolean; } -export interface DismissUndismissResponsePayload extends Identifier { +export interface DismissUndismissResponsePayload { insightId: string; + id: string; status: "success" | "failure"; error?: string; } diff --git a/src/components/Insights/types.ts b/src/components/Insights/types.ts index 93d18d06e..339022eb6 100644 --- a/src/components/Insights/types.ts +++ b/src/components/Insights/types.ts @@ -1,11 +1,6 @@ import { MemoExoticComponent } from "react"; import { Duration } from "../../globals"; -import { - Identifier, - InsightType, - SpanInfo, - SpanInstanceInfo -} from "../../types"; +import { InsightType, SpanInfo, SpanInstanceInfo } from "../../types"; import { Sorting } from "../common/SortingSelector/types"; import { IconProps } from "../common/icons/types"; import { InsightFilterType } from "./InsightsCatalog/types"; @@ -684,6 +679,7 @@ export interface ScopedInsightsQuery extends InsightsQuery { export { InsightType }; -export interface DismissUndismissInsightPayload extends Identifier { +export interface DismissUndismissInsightPayload { insightId: string; + id: string; } diff --git a/src/components/common/icons/16px/EyeIcon.tsx b/src/components/common/icons/16px/EyeIcon.tsx index 0c1a593a3..4dee07e3a 100644 --- a/src/components/common/icons/16px/EyeIcon.tsx +++ b/src/components/common/icons/16px/EyeIcon.tsx @@ -1,4 +1,4 @@ -import { forwardRef } from "react"; +import React from "react"; import { useIconProps } from "../hooks"; import { IconProps } from "../types"; @@ -34,4 +34,4 @@ const EyeIconComponent = (props: EyeIconProps) => { ); }; -export const EyeIcon = forwardRef(EyeIconComponent); +export const EyeIcon = React.memo(EyeIconComponent); diff --git a/src/hooks/useAction.ts b/src/hooks/useAction.ts index 89952ce32..9b62d9a8b 100644 --- a/src/hooks/useAction.ts +++ b/src/hooks/useAction.ts @@ -1,6 +1,9 @@ import { useCallback, useEffect, useState } from "react"; import { dispatcher } from "../dispatcher"; -import { Identifier } from "../types"; + +interface Identifier { + id: string; +} export const useAction = < TPayload extends Identifier, diff --git a/src/types.ts b/src/types.ts index baf4518f6..49c5f9b0c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -136,7 +136,3 @@ export interface SendPluginEventPayload { name: string; payload?: Record; } - -export interface Identifier { - id: string; -}