From 6d1d786a36757498cef4d0f26684dce57e60a431 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Wed, 16 Oct 2024 08:04:28 +0200 Subject: [PATCH 1/4] Add global error filters --- package.json | 4 +- src/components/Assets/AssetsFilter/styles.ts | 33 -- .../Dashboard/MetricsReport/Header/index.tsx | 2 +- .../Dashboard/MetricsReport/Table/index.tsx | 2 +- .../GlobalErrorsFilters.stories.tsx | 20 ++ .../GlobalErrorsFilters/index.tsx | 319 ++++++++++++++++++ .../GlobalErrorsFilters/styles.ts | 11 + .../GlobalErrorsFilters/types.ts | 24 ++ .../Errors/GlobalErrorsList/index.tsx | 68 +++- .../Errors/GlobalErrorsList/types.ts | 11 +- src/components/Errors/NewErrorCard/index.tsx | 48 ++- src/components/Errors/actions.ts | 4 +- src/components/Errors/tracking.ts | 12 + .../Insights/Issues/IssuesFilter/styles.ts | 24 -- src/components/common/FilterPopup/index.tsx | 1 + src/featureFlags.ts | 3 +- src/store/errors/errorsSlice.ts | 44 ++- src/types.ts | 3 +- 18 files changed, 532 insertions(+), 101 deletions(-) create mode 100644 src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/GlobalErrorsFilters.stories.tsx create mode 100644 src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx create mode 100644 src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/styles.ts create mode 100644 src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/types.ts diff --git a/package.json b/package.json index 90f810686..c8313e555 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "jest", "test:coverage": "jest --coverage", "storybook": "storybook dev -p 6006", + "start": "npm run storybook", "build-storybook": "storybook build", "build:dashboard:dev": "webpack --config webpack.dev.ts --env app=dashboard", "build:documentation:dev": "webpack --config webpack.dev.ts --env app=documentation", @@ -31,8 +32,7 @@ "build:prod": "webpack --config webpack.prod.ts", "build:prod:web": "webpack --config webpack.prod.ts --env platform=Web", "precommit": "lint-staged", - "prepare": "husky", - "start": "npm run storybook" + "prepare": "husky" }, "lint-staged": { "*.{js,ts,tsx}": [ diff --git a/src/components/Assets/AssetsFilter/styles.ts b/src/components/Assets/AssetsFilter/styles.ts index 438e8263a..5f4de3145 100644 --- a/src/components/Assets/AssetsFilter/styles.ts +++ b/src/components/Assets/AssetsFilter/styles.ts @@ -1,39 +1,6 @@ import styled from "styled-components"; -import { grayScale } from "../../common/App/v2colors"; import { Select } from "../../common/v3/Select"; -export const Container = styled.div` - display: flex; - flex-direction: column; - gap: 8px; - padding: 8px; - border-radius: 4px; - background: ${({ theme }) => theme.colors.surface.primary}; - box-shadow: 0 2px 4px 0 rgb(0 0 0 / 29%); - font-size: 14px; - color: ${grayScale[400]}; -`; - -export const Header = styled.div` - color: ${({ theme }) => theme.colors.select.menu.text.primary}; - padding: 0 4px; - display: flex; - align-items: center; -`; - -export const FilterCategoryName = styled.div` - display: flex; - padding: 4px; - color: ${grayScale[400]}; -`; - -export const Footer = styled.div` - padding: 8px 0; - display: flex; - justify-content: space-between; - align-items: center; -`; - export const StyledSelect = styled(Select)` background: ${({ theme }) => theme.colors.surface.brandDark}; `; diff --git a/src/components/Dashboard/MetricsReport/Header/index.tsx b/src/components/Dashboard/MetricsReport/Header/index.tsx index c688ca65d..f3940c8aa 100644 --- a/src/components/Dashboard/MetricsReport/Header/index.tsx +++ b/src/components/Dashboard/MetricsReport/Header/index.tsx @@ -239,7 +239,7 @@ export const Header = ({ onGoBack }: HeaderProps) => { const title = viewLevel === "endpoints" ? `${selectedService ?? ""} Service` - : "Services with Issues map"; + : "Issues Map"; const titleSuffix = viewLevel === "endpoints" ? " Endpoints" : ""; const tooltipTitle = `${title} ${titleSuffix}`; diff --git a/src/components/Dashboard/MetricsReport/Table/index.tsx b/src/components/Dashboard/MetricsReport/Table/index.tsx index a417bcc0a..ed91bac4a 100644 --- a/src/components/Dashboard/MetricsReport/Table/index.tsx +++ b/src/components/Dashboard/MetricsReport/Table/index.tsx @@ -134,7 +134,7 @@ export const Table = ({ } }), columnHelper.accessor((row) => row, { - header: "Critical issues", + header: "Issues", id: "issues", cell: (info) => { const value = info.getValue(); diff --git a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/GlobalErrorsFilters.stories.tsx b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/GlobalErrorsFilters.stories.tsx new file mode 100644 index 000000000..ef2085d24 --- /dev/null +++ b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/GlobalErrorsFilters.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { GlobalErrorsFilters } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "Errors/GlobalErrorsList/GlobalErrorsFilters", + component: GlobalErrorsFilters, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Default: Story = {}; diff --git a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx new file mode 100644 index 000000000..ed44839a8 --- /dev/null +++ b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx @@ -0,0 +1,319 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useFetchData } from "../../../../hooks/useFetchData"; +import { usePrevious } from "../../../../hooks/usePrevious"; +import { useConfigSelector } from "../../../../store/config/useConfigSelector"; +import { + ErrorFilter, + GlobalErrorsFiltersState +} from "../../../../store/errors/errorsSlice"; +import { useErrorsSelector } from "../../../../store/errors/useErrorsSelector"; +import { useStore } from "../../../../store/useStore"; +import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent"; +import { FilterPopup } from "../../../common/FilterPopup"; +import { WrenchIcon } from "../../../common/icons/12px/WrenchIcon"; +import { CrossCircleIcon } from "../../../common/icons/CrossCircleIcon"; +import { EndpointIcon } from "../../../common/icons/EndpointIcon"; +import { IconProps } from "../../../common/icons/types"; +import { SelectItem } from "../../../common/v3/Select/types"; +import { actions } from "../../actions"; +import { trackingEvents } from "../../tracking"; +import * as s from "./styles"; +import { + EndpointFilterData, + GetGlobalErrorsFiltersDataPayload, + SetGlobalErrorsFiltersDataPayload +} from "./types"; + +export const GlobalErrorsFilters = () => { + const { environment } = useConfigSelector(); + const { globalErrorsFilters, globalErrorsSelectedFilters } = + useErrorsSelector(); + const { setGlobalErrorsFilters, setGlobalErrorsSelectedFilters } = + useStore.getState(); + const { services, endpoints, errorTypes } = globalErrorsFilters; + const environmentId = environment?.id; + const [lastChangedFilter, setLastChangedFilter] = useState< + ErrorFilter | undefined + >(undefined); + const [selectedServices, setSelectedServices] = useState( + globalErrorsSelectedFilters.services + ); + const [selectedEndpoints, setSelectedEndpoints] = useState( + globalErrorsSelectedFilters.endpoints + ); + const [selectedErrorTypes, setSelectedErrorTypes] = useState( + globalErrorsSelectedFilters.errorTypes + ); + + const getLastSelectedFilterValues = useCallback( + (changedFilter: ErrorFilter) => { + switch (changedFilter) { + case "Services": + return selectedServices; + case "Endpoints": + return selectedEndpoints; + case "ErrorTypes": + return selectedErrorTypes; + } + }, + [selectedServices, selectedEndpoints, selectedErrorTypes] + ); + + const payload: GetGlobalErrorsFiltersDataPayload = useMemo( + () => ({ + environment: environmentId ?? "", + ...(lastChangedFilter + ? { + filterName: lastChangedFilter, + filterData: { + values: getLastSelectedFilterValues(lastChangedFilter), + ...(lastChangedFilter === "Endpoints" + ? { services: globalErrorsSelectedFilters.services } + : {}) + } + } + : {}) + }), + [ + environmentId, + globalErrorsSelectedFilters, + getLastSelectedFilterValues, + lastChangedFilter + ] + ); + + const { data } = useFetchData< + GetGlobalErrorsFiltersDataPayload, + SetGlobalErrorsFiltersDataPayload + >( + { + requestAction: actions.GET_GLOBAL_ERRORS_FILTERS_DATA, + responseAction: actions.SET_GLOBAL_ERRORS_FILTERS_DATA, + refreshWithInterval: false, + refreshOnPayloadChange: true, + isEnabled: Boolean(environment) + }, + payload + ); + const previousData = usePrevious(data); + + useEffect(() => { + if (previousData !== data && data) { + const newServices = data.filters.find((x) => x.filterName === "Services"); + const newEndpoints = data.filters.find( + (x) => x.filterName === "Endpoints" + ); + const newErrorTypes = data.filters.find( + (x) => x.filterName === "ErrorTypes" + ); + + const newGlobalErrorsFilters: GlobalErrorsFiltersState = { + ...globalErrorsFilters + }; + + if (newServices) { + newGlobalErrorsFilters.services = newServices.values as string[]; + } + + if (newEndpoints) { + newGlobalErrorsFilters.endpoints = + newEndpoints.values as EndpointFilterData[]; + } + + if (newErrorTypes) { + newGlobalErrorsFilters.errorTypes = newErrorTypes.values as string[]; + } + + setGlobalErrorsFilters(newGlobalErrorsFilters); + } + }, [previousData, data, setGlobalErrorsFilters, globalErrorsFilters]); + + useEffect(() => { + setSelectedServices(globalErrorsSelectedFilters.services); + setSelectedEndpoints(globalErrorsSelectedFilters.endpoints); + setSelectedErrorTypes(globalErrorsSelectedFilters.errorTypes); + }, [ + globalErrorsSelectedFilters.services, + globalErrorsSelectedFilters.endpoints, + globalErrorsSelectedFilters.errorTypes + ]); + + const handleServicesChange = (value: string | string[]) => { + sendUserActionTrackingEvent( + trackingEvents.GLOBAL_ERRORS_VIEW_SERVICES_FILTER_CHANGED + ); + const newValue = Array.isArray(value) ? value : [value]; + setLastChangedFilter("Services"); + setSelectedServices(newValue); + setSelectedEndpoints([]); + setSelectedErrorTypes([]); + }; + + const handleEndpointsChange = (value: string | string[]) => { + sendUserActionTrackingEvent( + trackingEvents.GLOBAL_ERRORS_VIEW_ENDPOINTS_FILTER_CHANGED + ); + const newValue = Array.isArray(value) ? value : [value]; + setLastChangedFilter("Endpoints"); + setSelectedEndpoints(newValue); + setSelectedErrorTypes([]); + }; + + const handleErrorTypesChange = (value: string | string[]) => { + sendUserActionTrackingEvent( + trackingEvents.GLOBAL_ERRORS_VIEW_ERROR_TYPES_FILTER_CHANGED + ); + const newValue = Array.isArray(value) ? value : [value]; + setLastChangedFilter("Endpoints"); + setSelectedErrorTypes(newValue); + }; + + const servicesFilterOptions: SelectItem[] = + services?.map((x) => ({ + label: x, + value: x, + selected: selectedServices.includes(x), + enabled: true + })) ?? []; + + const servicesFilterPlaceholder = + servicesFilterOptions.filter((x) => x.selected).length > 0 + ? "Services" + : "All"; + + const endpointsFilterOptions: SelectItem[] = + endpoints?.map((x) => ({ + label: x.displayName, + value: x.spanCodeObjectId, + selected: selectedEndpoints.includes(x.spanCodeObjectId), + enabled: true + })) ?? []; + + const endpointsFilterPlaceholder = + endpointsFilterOptions.filter((x) => x.selected).length > 0 + ? "Endpoints" + : "All"; + + const errorTypesFilterOptions: SelectItem[] = + errorTypes?.map((x) => ({ + label: x, + value: x, + selected: selectedErrorTypes.includes(x), + enabled: true + })) ?? []; + + const errorTypesFilterPlaceholder = + errorTypesFilterOptions.filter((x) => x.selected).length > 0 + ? "Error types" + : "All"; + + const filters = [ + { + title: "Services", + component: ( + ( + + + + )} + disabled={servicesFilterOptions?.length === 0} + /> + ) + }, + { + title: "Endpoints", + component: ( + ( + + + + )} + disabled={endpointsFilterOptions?.length === 0} + /> + ) + }, + { + title: "Error type", + component: ( + ( + + + + )} + disabled={errorTypesFilterOptions?.length === 0} + /> + ) + } + ]; + + const applyFilters = () => { + setGlobalErrorsSelectedFilters({ + ...globalErrorsSelectedFilters, + services: selectedServices, + endpoints: selectedEndpoints, + errorTypes: selectedErrorTypes + }); + }; + + const handleClose = () => { + sendUserActionTrackingEvent( + trackingEvents.GLOBAL_ERRORS_VIEW_FILTERS_CLOSE_BUTTON_CLICKED + ); + setGlobalErrorsSelectedFilters({ + ...globalErrorsSelectedFilters, + services: selectedServices, + endpoints: selectedEndpoints, + errorTypes: selectedErrorTypes + }); + }; + + const handleClearAll = () => { + sendUserActionTrackingEvent( + trackingEvents.GLOBAL_ERRORS_VIEW_CLEAR_FILTERS_BUTTON_CLICKED + ); + setSelectedServices([]); + setSelectedEndpoints([]); + setSelectedErrorTypes([]); + }; + + const selectedFiltersCount = [ + selectedServices.length, + selectedEndpoints.length, + selectedErrorTypes.length + ].filter((x) => x > 0).length; + + const handlePopupOpenStateChange = (isOpen: boolean) => { + if (!isOpen) { + applyFilters(); + } + }; + + return ( + + ); +}; diff --git a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/styles.ts b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/styles.ts new file mode 100644 index 000000000..b374248d6 --- /dev/null +++ b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/styles.ts @@ -0,0 +1,11 @@ +import styled from "styled-components"; +import { Select } from "../../../common/v3/Select"; + +export const StyledSelect = styled(Select)` + background: ${({ theme }) => theme.colors.surface.brandDark}; +`; + +export const SelectItemIconContainer = styled.div` + display: flex; + color: ${({ theme }) => theme.colors.v3.icon.tertiary}; +`; diff --git a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/types.ts b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/types.ts new file mode 100644 index 000000000..f240817c2 --- /dev/null +++ b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/types.ts @@ -0,0 +1,24 @@ +import { ErrorFilter } from "../../../../store/errors/errorsSlice"; + +export interface GetGlobalErrorsFiltersDataPayload { + environment: string; + filterName?: string; + filterData?: { + services?: string[]; + values: string[]; + }; +} + +export interface FilterData { + filterName: ErrorFilter; + values: T[]; +} + +export interface EndpointFilterData { + spanCodeObjectId: string; + displayName: string; +} + +export interface SetGlobalErrorsFiltersDataPayload { + filters: FilterData[]; +} diff --git a/src/components/Errors/GlobalErrorsList/index.tsx b/src/components/Errors/GlobalErrorsList/index.tsx index 3c08a8b8c..94f672e2d 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 { getFeatureFlagValue } from "../../../featureFlags"; import { DataFetcherConfiguration, useFetchData @@ -11,6 +12,7 @@ import { } from "../../../store/errors/errorsSlice"; import { useErrorsSelector } from "../../../store/errors/useErrorsSelector"; import { useStore } from "../../../store/useStore"; +import { FeatureFlag } from "../../../types"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; import { OppositeArrowsIcon } from "../../common/icons/12px/OppositeArrowsIcon"; import { CardsColoredIcon } from "../../common/icons/CardsColoredIcon"; @@ -26,6 +28,7 @@ import { actions } from "../actions"; import { NewErrorCard } from "../NewErrorCard"; import { NoDataEmptyState } from "../NoDataEmptyState"; import { trackingEvents } from "../tracking"; +import { GlobalErrorsFilters } from "./GlobalErrorsFilters"; import * as s from "./styles"; import { GetGlobalErrorsDataPayload, @@ -37,14 +40,16 @@ export const GlobalErrorsList = () => { const [isSortingMenuOpen, setIsSortingMenuOpen] = useState(false); const listContainerRef = useRef(null); - const { environment } = useConfigSelector(); + const { environment, backendInfo } = useConfigSelector(); + const { globalErrorsSearch: search, globalErrorsSorting: sorting, globalErrorsPage: page, globalErrorsPageSize: pageSize, globalErrorsList: list, - globalErrorsTotalCount: totalCount + globalErrorsTotalCount: totalCount, + globalErrorsSelectedFilters: selectedFilters } = useErrorsSelector(); const { @@ -52,9 +57,15 @@ export const GlobalErrorsList = () => { setGlobalErrorsSearch, setGlobalErrorsSorting, setGlobalErrorsPage, - resetGlobalErrors + resetGlobalErrors, + resetGlobalErrorsSelectedFilters } = useStore.getState(); + const areGlobalErrorsFiltersEnabled = getFeatureFlagValue( + backendInfo, + FeatureFlag.ARE_GLOBAL_ERRORS_FILTERS_ENABLED + ); + const environmentId = environment?.id; const sortingMenuItems = Object.values(GLOBAL_ERROR_SORTING_CRITERION).map( @@ -71,9 +82,9 @@ export const GlobalErrorsList = () => { responseAction: actions.SET_GLOBAL_ERRORS_DATA, refreshWithInterval: true, refreshOnPayloadChange: true, - isEnabled: Boolean(environmentId) + isEnabled: Boolean(environmentId && areGlobalErrorsFiltersEnabled) }), - [environmentId] + [environmentId, areGlobalErrorsFiltersEnabled] ); const payload: GetGlobalErrorsDataPayload = useMemo( @@ -82,9 +93,24 @@ export const GlobalErrorsList = () => { searchCriteria: search, sortBy: sorting, page, - pageSize: PAGE_SIZE + pageSize: PAGE_SIZE, + services: selectedFilters.services, + endpoints: selectedFilters.endpoints, + errorTypes: selectedFilters.errorTypes, + criticality: selectedFilters.criticality, + handlingTypes: selectedFilters.handlingTypes }), - [environmentId, search, sorting, page] + [ + environmentId, + search, + sorting, + page, + selectedFilters.services, + selectedFilters.endpoints, + selectedFilters.errorTypes, + selectedFilters.criticality, + selectedFilters.handlingTypes + ] ); const { data } = useFetchData< @@ -102,7 +128,21 @@ export const GlobalErrorsList = () => { // Reset page on filters change useEffect(() => { setGlobalErrorsPage(0); - }, [environmentId, search, setGlobalErrorsPage]); + }, [ + environmentId, + search, + setGlobalErrorsPage, + selectedFilters.services, + selectedFilters.endpoints, + selectedFilters.errorTypes, + selectedFilters.criticality, + selectedFilters.handlingTypes + ]); + + // Reset filters on environment change + useEffect(() => { + resetGlobalErrorsSelectedFilters(); + }, [environmentId, resetGlobalErrorsSelectedFilters]); // Reset scroll position on filters change useEffect(() => { @@ -155,15 +195,25 @@ export const GlobalErrorsList = () => { ); setGlobalErrorsSearch(""); setGlobalErrorsPage(0); + resetGlobalErrorsSelectedFilters(); }; - const areAnyFiltersApplied = search; + const areAnyFiltersApplied = + search || + [ + selectedFilters.services, + selectedFilters.endpoints, + selectedFilters.errorTypes, + selectedFilters.criticality, + selectedFilters.handlingTypes + ].some((x) => x.length > 0); return ( {list ? ( <> + {areGlobalErrorsFiltersEnabled && } { - const statusString = status.toLowerCase(); - if (statusString.localeCompare("high number of errors")) { - return "highSeverity"; - } - - if (statusString.localeCompare("escalating")) { - return "mediumSeverity"; - } - - if (statusString.includes("recent")) { - return "lowSeverity"; - } - - return "default"; -}; - export const NewErrorCard = ({ data, onSourceLinkClick @@ -43,8 +25,8 @@ export const NewErrorCard = ({ fromFullyQualifiedName, status } = data; - const statusTagType = getStatusTagType(status); - const selectorOptions = useMemo( + const statusTagType = getTagType(score.score); + const selectorOptions: Option[] = useMemo( () => affectedEndpoints.map((x) => ({ route: x.displayName, @@ -53,10 +35,20 @@ export const NewErrorCard = ({ })), [affectedEndpoints] ); - const [selectedEndpoint, setSelectedEndpoint] = useState( - selectorOptions.length > 0 ? selectorOptions[0] : undefined + const [selectedEndpoint, setSelectedEndpoint] = useState