diff --git a/.eslintrc.js b/.eslintrc.js index c3c055859..1de8c9e9b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,8 +36,7 @@ module.exports = { }, overrides: [ { - files: ["./**/*.tsx"], - excludedFiles: ["./src/common/icons/**/*Icon.tsx"], + files: ["./src/components/common/icons/**/*Icon.tsx"], rules: { "react/jsx-curly-brace-presence": "off" } diff --git a/src/components/Assets/AssetList/index.tsx b/src/components/Assets/AssetList/index.tsx index d5a9bae19..a005a3a20 100644 --- a/src/components/Assets/AssetList/index.tsx +++ b/src/components/Assets/AssetList/index.tsx @@ -144,7 +144,13 @@ export const AssetList = ({ sortOrder: sorting.order, scopedSpanCodeObjectId: scopeSpanCodeObjectId, ...(search.length > 0 ? { displayName: search } : {}), - ...(scopeSpanCodeObjectId ? { ...filters, services: [] } : filters), + insights: filters?.insights ?? [], + operations: [ + ...(filters?.endpoints ?? []), + ...(filters?.consumers ?? []), + ...(filters?.internals ?? []) + ], + services: scopeSpanCodeObjectId ? [] : filters?.services ?? [], directOnly: viewMode === "children" } }), diff --git a/src/components/Assets/AssetTypeList/index.tsx b/src/components/Assets/AssetTypeList/index.tsx index 64143134a..9b1dfb11a 100644 --- a/src/components/Assets/AssetTypeList/index.tsx +++ b/src/components/Assets/AssetTypeList/index.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { DigmaMessageError } from "../../../api/types"; import { dispatcher } from "../../../dispatcher"; import { usePrevious } from "../../../hooks/usePrevious"; +import { AssetsFilters } from "../../../store/assets/assetsSlice"; import { useAssetsSelector } from "../../../store/assets/useAssetsSelector"; import { useConfigSelector } from "../../../store/config/useConfigSelector"; import { useStore } from "../../../store/useStore"; @@ -12,7 +13,6 @@ import { SCOPE_CHANGE_EVENTS } from "../../../types"; import { changeScope } from "../../../utils/actions/changeScope"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; import { ChildIcon } from "../../common/icons/30px/ChildIcon"; -import { AssetFilterQuery } from "../AssetsFilter/types"; import { ViewMode } from "../AssetsViewScopeConfiguration/types"; import { NoDataMessage } from "../NoDataMessage"; import { actions } from "../actions"; @@ -23,7 +23,8 @@ import * as s from "./styles"; import { AssetCategoriesData, AssetCategoryData, - AssetTypeListProps + AssetTypeListProps, + GetAssetCategoriesDataPayload } from "./types"; const REFRESH_INTERVAL = 10 * 1000; // in milliseconds @@ -40,23 +41,26 @@ export const ASSET_TYPE_IDS = [ ]; const getData = ( - filters: AssetFilterQuery, + filters: AssetsFilters | null, searchQuery: string, viewMode: ViewMode, scopeSpanCodeObjectId?: string ) => { - window.sendMessageToDigma({ + window.sendMessageToDigma({ action: actions.GET_CATEGORIES_DATA, payload: { query: { directOnly: viewMode === "children", scopedSpanCodeObjectId: scopeSpanCodeObjectId, - ...(scopeSpanCodeObjectId - ? { - ...filters, - services: [] - } - : filters), + ...{ + insights: filters?.insights ?? [], + operations: [ + ...(filters?.endpoints ?? []), + ...(filters?.consumers ?? []), + ...(filters?.internals ?? []) + ], + services: scopeSpanCodeObjectId ? [] : filters?.services ?? [] + }, ...(searchQuery.length > 0 ? { displayName: searchQuery } : {}) } } diff --git a/src/components/Assets/AssetTypeList/types.ts b/src/components/Assets/AssetTypeList/types.ts index b4f2e3b07..457766206 100644 --- a/src/components/Assets/AssetTypeList/types.ts +++ b/src/components/Assets/AssetTypeList/types.ts @@ -21,3 +21,14 @@ export interface AssetCategoryData { label?: string; icon?: MemoExoticComponent<(props: IconProps) => JSX.Element>; } + +export interface GetAssetCategoriesDataPayload { + query: { + directOnly: boolean; + scopedSpanCodeObjectId?: string; + services: string[]; + operations: string[]; + insights: string[]; + displayName?: string; + }; +} diff --git a/src/components/Assets/AssetsFilter/index.tsx b/src/components/Assets/AssetsFilter/index.tsx index 08e8fba0c..d94b03bff 100644 --- a/src/components/Assets/AssetsFilter/index.tsx +++ b/src/components/Assets/AssetsFilter/index.tsx @@ -1,14 +1,18 @@ -import { ComponentType, useEffect, useMemo, useState } from "react"; +import { + ComponentType, + useCallback, + useEffect, + useMemo, + useState +} from "react"; import { dispatcher } from "../../../dispatcher"; import { getFeatureFlagValue } from "../../../featureFlags"; -import { usePersistence } from "../../../hooks/usePersistence"; import { usePrevious } from "../../../hooks/usePrevious"; import { useAssetsSelector } from "../../../store/assets/useAssetsSelector"; import { useConfigSelector } from "../../../store/config/useConfigSelector"; import { useStore } from "../../../store/useStore"; import { isEnvironment } from "../../../typeGuards/isEnvironment"; import { isNull } from "../../../typeGuards/isNull"; -import { isUndefined } from "../../../typeGuards/isUndefined"; import { FeatureFlag, InsightType } from "../../../types"; import { sendTrackingEvent } from "../../../utils/actions/sendTrackingEvent"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; @@ -23,14 +27,11 @@ import { trackingEvents } from "../tracking"; import * as s from "./styles"; import { AssetFilterCategory, - AssetFilterQuery, AssetsFiltersData, GetAssetFiltersDataParams, GetAssetFiltersDataPayload } from "./types"; -const PERSISTENCE_KEY = "assetsFilters"; - const getData = ({ services, operations, @@ -92,17 +93,15 @@ export const AssetsFilter = () => { setAssetsFilters: setFilters, setSelectedServices: setGloballySelectedServices } = useStore.getState(); - const [isOpen, setIsOpen] = useState(false); - const previousIsOpen = usePrevious(isOpen); + const [isPopupOpen, setIsPopupOpen] = useState(false); + const previousIsOpen = usePrevious(isPopupOpen); const { selectedServices: globallySelectedServices, environment, scope, backendInfo } = useConfigSelector(); - const [persistedFilters, setPersistedFilters] = - usePersistence(PERSISTENCE_KEY, "project"); - const previousPersistedFilters = usePrevious(persistedFilters); + const isServicesFilterEnabled = !scope?.span?.spanCodeObjectId; const [selectedServices, setSelectedServices] = useState( isServicesFilterEnabled ? globallySelectedServices ?? [] : [] @@ -122,8 +121,12 @@ export const AssetsFilter = () => { const query = useMemo( () => ({ services: isServicesFilterEnabled ? selectedServices : [], - operations: filters.operations, - insights: filters.insights as InsightType[], + operations: [ + ...selectedEndpoints, + ...selectedConsumers, + ...selectedInternals + ], + insights: selectedInsights, viewMode: areExtendedAssetsFiltersEnabled ? viewMode : undefined, scopeSpanCodeObjectId: areExtendedAssetsFiltersEnabled ? scopeSpanCodeObjectId @@ -133,8 +136,10 @@ export const AssetsFilter = () => { [ isServicesFilterEnabled, selectedServices, - filters.operations, - filters.insights, + selectedEndpoints, + selectedConsumers, + selectedInternals, + selectedInsights, viewMode, searchQuery, scopeSpanCodeObjectId, @@ -142,36 +147,6 @@ export const AssetsFilter = () => { ] ); - // Get data after filters have been rehydrated - useEffect(() => { - if ( - isUndefined(previousPersistedFilters) && - !isUndefined(persistedFilters) - ) { - getData({ - ...query, - services: isServicesFilterEnabled ? selectedServices : [], - operations: persistedFilters?.operations ?? [ - ...selectedEndpoints, - ...selectedConsumers, - ...selectedInternals - ], - insights: - (persistedFilters?.insights as InsightType[]) || selectedInsights - }); - } - }, [ - previousPersistedFilters, - persistedFilters, - selectedServices, - selectedEndpoints, - selectedConsumers, - selectedInternals, - selectedInsights, - isServicesFilterEnabled, - query - ]); - // Handle filters data response useEffect(() => { const handleData = (data: unknown) => { @@ -195,13 +170,20 @@ export const AssetsFilter = () => { isEnvironment(previousEnvironment) && previousEnvironment.id !== environment?.id ) { + setSelectedServices([]); + setSelectedEndpoints([]); + setSelectedConsumers([]); + setSelectedInternals([]); + setSelectedInsights([]); + const defaultFilters = { services: [], - operations: [], + endpoints: [], + consumers: [], + internals: [], insights: [] }; setFilters(defaultFilters); - setPersistedFilters(defaultFilters); if (isServicesFilterEnabled) { setGloballySelectedServices(defaultFilters.services); } @@ -214,10 +196,8 @@ export const AssetsFilter = () => { setFilters, previousEnvironment, environment, - setPersistedFilters, isServicesFilterEnabled, setGloballySelectedServices, - areExtendedAssetsFiltersEnabled, query ]); @@ -227,41 +207,83 @@ export const AssetsFilter = () => { previousScope && previousScope.span?.spanCodeObjectId !== scopeSpanCodeObjectId ) { - const newFilters = { - services: selectedServices, - operations: [], - insights: [] - }; - setPersistedFilters(newFilters); + setSelectedEndpoints([]); + setSelectedConsumers([]); + setSelectedInternals([]); + setSelectedInsights([]); + if (isServicesFilterEnabled) { - setGloballySelectedServices(newFilters.services); + setGloballySelectedServices(selectedServices); } - setFilters(newFilters); + setFilters({ + services: selectedServices, + endpoints: [], + consumers: [], + internals: [], + insights: [] + }); getData({ ...query, - ...newFilters + services: selectedServices, + operations: [], + insights: [] }); } }, [ setFilters, - setPersistedFilters, setGloballySelectedServices, previousScope, scopeSpanCodeObjectId, selectedServices, isServicesFilterEnabled, - areExtendedAssetsFiltersEnabled, query ]); + const discardChanges = useCallback(() => { + setSelectedServices(filters?.services ?? []); + setSelectedEndpoints(filters?.endpoints ?? []); + setSelectedConsumers(filters?.consumers ?? []); + setSelectedInternals(filters?.internals ?? []); + setSelectedInsights((filters?.insights as InsightType[]) ?? []); + + getData({ + ...query, + services: filters?.services ?? [], + operations: [ + ...(filters?.endpoints ?? []), + ...(filters?.consumers ?? []), + ...(filters?.internals ?? []) + ], + insights: (filters?.insights as InsightType[]) ?? [] + }); + }, [filters, query]); + + // Close popup on environment or scope changes + useEffect(() => { + if ( + previousEnvironment?.id !== environment?.id || + previousScope?.span?.spanCodeObjectId !== scopeSpanCodeObjectId + ) { + setIsPopupOpen(false); + + discardChanges(); + } + }, [ + environment, + scopeSpanCodeObjectId, + previousEnvironment, + previousScope, + discardChanges + ]); + // Get data when the popover is opened useEffect(() => { - if (isOpen && !previousIsOpen) { + if (isPopupOpen && !previousIsOpen) { getData(query); } - }, [isOpen, previousIsOpen, query]); + }, [isPopupOpen, previousIsOpen, query]); - // Apply filters when data is loaded + // Update selected filters when data is fetched useEffect(() => { if (previousData === data || isNull(data)) { return; @@ -304,73 +326,11 @@ export const AssetsFilter = () => { ?.entries?.filter((x) => x.selected) .map((x) => x.name) ?? []) as InsightType[]; setSelectedInsights(insightsToSelect); - - if (!filters) { - const filtersQuery = { - services: servicesToSelect, - operations: [ - ...endpointsToSelect, - ...consumersToSelect, - ...internalsToSelect - ], - insights: insightsToSelect - }; - - setPersistedFilters(filtersQuery); - if (isServicesFilterEnabled) { - setGloballySelectedServices(filtersQuery.services); - } - setFilters(filtersQuery); - } - }, [ - previousData, - data, - filters, - setFilters, - selectedServices, - selectedEndpoints, - selectedConsumers, - selectedInternals, - selectedInsights, - setPersistedFilters, - setGloballySelectedServices, - isServicesFilterEnabled - ]); - - // Apply filters when the popover is closed - useEffect(() => { - if (previousIsOpen && !isOpen) { - const filtersQuery = { - services: isServicesFilterEnabled ? selectedServices : [], - operations: [ - ...selectedEndpoints, - ...selectedConsumers, - ...selectedInternals - ], - insights: selectedInsights - }; - setFilters(filtersQuery); - setPersistedFilters(filtersQuery); - if (isServicesFilterEnabled) { - setGloballySelectedServices(filtersQuery.services); - } - sendTrackingEvent(trackingEvents.FILTER_APPLIED); - } - }, [ - previousIsOpen, - isOpen, - selectedConsumers, - selectedEndpoints, - selectedInsights, - selectedInternals, - selectedServices, - setPersistedFilters, - setGloballySelectedServices, - isServicesFilterEnabled, - setFilters - ]); + }, [previousData, data]); const handleClearFiltersButtonClick = () => { + sendUserActionTrackingEvent(trackingEvents.CLEAR_FILTERS_BUTTON_CLICKED); + getData({ ...query, services: isServicesFilterEnabled ? [] : selectedServices, @@ -453,21 +413,66 @@ export const AssetsFilter = () => { entries: [] }; - const selectedFilters = [ + const selectedFiltersCount = [ ...(isServicesFilterEnabled ? selectedServices : []), ...selectedEndpoints, ...selectedConsumers, ...selectedInternals, ...selectedInsights - ]; + ].length; + + const appliedFiltersCount = filters + ? [ + ...(isServicesFilterEnabled ? filters.services : []), + ...filters.endpoints, + ...filters.consumers, + ...filters.internals, + ...filters.insights + ].length + : 0; const handleCloseButtonClick = () => { sendUserActionTrackingEvent( trackingEvents.FILTERS_POPUP_CLOSE_BUTTON_CLICKED ); + + setIsPopupOpen(false); + + discardChanges(); + }; + + const handleFiltersButtonClick = () => { + sendUserActionTrackingEvent(trackingEvents.FILTERS_BUTTON_CLICKED); + + setIsPopupOpen(!isPopupOpen); + + if (isPopupOpen) { + discardChanges(); + } }; - const handleOnStateChange = (state: boolean) => { - setIsOpen(state); + + const handleApplyButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.FILTERS_POPUP_APPLY_FILTERS_BUTTON_CLICKED + ); + + setIsPopupOpen(false); + + const newServices = isServicesFilterEnabled ? selectedServices : []; + + setFilters({ + services: newServices, + endpoints: selectedEndpoints, + consumers: selectedConsumers, + internals: selectedInternals, + insights: selectedInsights + }); + + if (isServicesFilterEnabled) { + setGloballySelectedServices(newServices); + } + + sendTrackingEvent(trackingEvents.FILTER_APPLIED); }; const filterComponents = [ @@ -530,12 +535,15 @@ export const AssetsFilter = () => { return ( ); }; diff --git a/src/components/Assets/index.tsx b/src/components/Assets/index.tsx index be77b0f3c..0374f04a1 100644 --- a/src/components/Assets/index.tsx +++ b/src/components/Assets/index.tsx @@ -1,10 +1,13 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import { useDebounce } from "../../hooks/useDebounce"; +import { usePersistence } from "../../hooks/usePersistence"; import { usePrevious } from "../../hooks/usePrevious"; +import { AssetsFilters } from "../../store/assets/assetsSlice"; import { useAssetsSelector } from "../../store/assets/useAssetsSelector"; import { useConfigSelector } from "../../store/config/useConfigSelector"; import { useStore } from "../../store/useStore"; +import { isUndefined } from "../../typeGuards/isUndefined"; import { sendUserActionTrackingEvent } from "../../utils/actions/sendUserActionTrackingEvent"; import { useHistory } from "../Main/useHistory"; import { EmptyState } from "../common/EmptyState"; @@ -22,6 +25,7 @@ import * as s from "./styles"; import { trackingEvents } from "./tracking"; import { DataRefresher } from "./types"; +const PERSISTENCE_KEY = "assetsFiltersV2"; const SEARCH_INPUT_DEBOUNCE_DELAY = 1000; // in milliseconds const getAssetCategoryCount = ( @@ -33,11 +37,17 @@ const getAssetCategoryCount = ( ) ?? 0; export const Assets = () => { + const [persistedFilters, setPersistedFilters] = usePersistence( + PERSISTENCE_KEY, + "project" + ); + const previousPersistedFilters = usePrevious(persistedFilters); const [assetsCount, setAssetsCount] = useState(); const params = useParams(); const selectedAssetTypeId = useMemo(() => params.typeId, [params]); const { search, filters, assets, assetCategoriesData } = useAssetsSelector(); - const { setAssetsSearch: setSearch } = useStore.getState(); + const { setAssetsSearch: setSearch, setAssetsFilters: setFilters } = + useStore.getState(); const [searchInputValue, setSearchInputValue] = useState(search); const debouncedSearchInputValue = useDebounce( searchInputValue, @@ -53,6 +63,7 @@ export const Assets = () => { const { goTo } = useHistory(); const isBackendUpgradeMessageVisible = false; const { showAssetsHeaderToolBox } = useAssetsSelector(); + const isInitialized = Boolean(filters); useEffect(() => { if (previousScopeSpanCodeObjectId !== scopeSpanCodeObjectId) { @@ -64,6 +75,23 @@ export const Assets = () => { setSearch(debouncedSearchInputValue); }, [debouncedSearchInputValue, setSearch]); + useEffect(() => { + if ( + isUndefined(previousPersistedFilters) && + !isUndefined(persistedFilters) && + persistedFilters + ) { + setFilters(persistedFilters); + } + }, [previousPersistedFilters, persistedFilters, setFilters]); + + // Update persisted filters on filters change + useEffect(() => { + if (isInitialized) { + setPersistedFilters(filters); + } + }, [isInitialized, filters, setPersistedFilters]); + const handleGoToAllAssets = () => { goTo(".."); }; @@ -155,6 +183,10 @@ export const Assets = () => { ); }; + if (!isInitialized) { + return null; + } + return ( diff --git a/src/components/Assets/tracking.ts b/src/components/Assets/tracking.ts index 4ddd92ccf..393b6c7c9 100644 --- a/src/components/Assets/tracking.ts +++ b/src/components/Assets/tracking.ts @@ -8,7 +8,11 @@ export const trackingEvents = addPrefix( FILTER_APPLIED: "filter applied", REFRESH_BUTTON_CLICKED: "refresh button clicked", FILTERS_POPUP_CLOSE_BUTTON_CLICKED: "filter popup close button clicked", - ALL_ASSETS_LINK_CLICKED: "all assets link clicked" + FILTERS_POPUP_APPLY_FILTERS_BUTTON_CLICKED: + "filter popup apply filters button clicked", + ALL_ASSETS_LINK_CLICKED: "all assets link clicked", + FILTERS_BUTTON_CLICKED: "filters button clicked", + CLEAR_FILTERS_BUTTON_CLICKED: "clear filters button clicked" }, " " ); diff --git a/src/components/Assets/utils.tsx b/src/components/Assets/utils.tsx index 721a69313..abc70fda4 100644 --- a/src/components/Assets/utils.tsx +++ b/src/components/Assets/utils.tsx @@ -1,11 +1,11 @@ import { MemoExoticComponent } from "react"; +import { AssetsFilters } from "../../store/assets/assetsSlice"; import { CodeMarkerPinIcon } from "../common/icons/CodeMarkerPinIcon"; import { DatabaseIcon } from "../common/icons/DatabaseIcon"; import { EndpointIcon } from "../common/icons/EndpointIcon"; import { HTTPClientIcon } from "../common/icons/HTTPClientIcon"; import { UserIcon } from "../common/icons/UserIcon"; import { IconProps } from "../common/icons/types"; -import { AssetFilterQuery } from "./AssetsFilter/types"; interface AssetTypeInfo { label: string; @@ -53,14 +53,17 @@ export const getAssetTypeInfo = ( }; export const checkIfAnyFiltersApplied = ( - filters: AssetFilterQuery, + filters: AssetsFilters | null, searchQuery: string, isServicesFilterEnabled: boolean ) => Boolean( - [ - ...filters.insights, - ...filters.operations, - ...(isServicesFilterEnabled ? filters.services : []) - ].length > 0 + filters && + [ + ...filters.insights, + ...filters.endpoints, + ...filters.consumers, + ...filters.internals, + ...(isServicesFilterEnabled ? filters.services : []) + ].length > 0 ) || searchQuery.length > 0; diff --git a/src/components/Errors/GlobalErrorsList/DaysFilter/index.tsx b/src/components/Errors/GlobalErrorsList/DaysFilter/index.tsx index c008d4fa2..445ab69b4 100644 --- a/src/components/Errors/GlobalErrorsList/DaysFilter/index.tsx +++ b/src/components/Errors/GlobalErrorsList/DaysFilter/index.tsx @@ -120,8 +120,8 @@ export const DaysFilter = ({ onChanged }: DaysFilterProps) => { } + buttonType={"secondaryBorderless"} + icon={() => } onClick={handleDecrement} /> { value={currentValue?.toString()} /> } + buttonType={"secondaryBorderless"} + icon={() => } onClick={handleIncrement} /> Last days diff --git a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx index 7de4e6933..6f89a254a 100644 --- a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx +++ b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx @@ -34,25 +34,36 @@ const getSelectPlaceholder = (options: SelectItem[], placeholder: string) => options.filter((x) => x.selected).length > 0 ? placeholder : "All"; export const GlobalErrorsFilters = () => { - const { environment, backendInfo } = useConfigSelector(); - + const [isPopupOpen, setIsPopupOpen] = useState(false); + const { + environment, + backendInfo, + selectedServices: globallySelectedServices, + scope + } = useConfigSelector(); const areGlobalErrorsCriticalityAndUnhandledFiltersEnabled = getFeatureFlagValue( backendInfo, FeatureFlag.ARE_GLOBAL_ERRORS_CRITICALITY_AND_UNHANDLED_FILTERS_ENABLED ); - const { globalErrorsFilters, globalErrorsSelectedFilters } = useErrorsSelector(); - const { setGlobalErrorsFilters, setGlobalErrorsSelectedFilters } = - useStore.getState(); + const { + setGlobalErrorsFilters, + setGlobalErrorsSelectedFilters, + setSelectedServices: setGloballySelectedServices + } = useStore.getState(); const { services, endpoints, errorTypes } = globalErrorsFilters; const environmentId = environment?.id; + const previousEnvironmentId = usePrevious(environmentId); + const scopeSpanCodeObjectId = scope?.span?.spanCodeObjectId; + const previousScopeSpanCodeObjectId = usePrevious(scopeSpanCodeObjectId); + const [lastChangedFilter, setLastChangedFilter] = useState< ErrorFilter | undefined >(undefined); const [selectedServices, setSelectedServices] = useState( - globalErrorsSelectedFilters.services + globallySelectedServices ?? [] ); const [selectedEndpoints, setSelectedEndpoints] = useState( globalErrorsSelectedFilters.endpoints @@ -160,6 +171,48 @@ export const GlobalErrorsFilters = () => { globalErrorsSelectedFilters.errorTypes ]); + // Clear filters on environment change, but keep the selected services + useEffect(() => { + if (previousEnvironmentId !== environmentId) { + setSelectedEndpoints([]); + setSelectedErrorTypes([]); + setSelectedCriticalities([]); + setSelectedHandlingTypes([]); + } + }, [environmentId, previousEnvironmentId]); + + const discardChanges = useCallback(() => { + setSelectedServices(globalErrorsSelectedFilters.services); + setSelectedEndpoints(globalErrorsSelectedFilters.endpoints); + setSelectedErrorTypes(globalErrorsSelectedFilters.errorTypes); + setSelectedCriticalities(globalErrorsSelectedFilters.criticalities); + setSelectedHandlingTypes(globalErrorsSelectedFilters.handlingTypes); + }, [ + globalErrorsSelectedFilters.services, + globalErrorsSelectedFilters.endpoints, + globalErrorsSelectedFilters.errorTypes, + globalErrorsSelectedFilters.criticalities, + globalErrorsSelectedFilters.handlingTypes + ]); + + // Close popup on environment or scope changes + useEffect(() => { + if ( + previousEnvironmentId !== environmentId || + previousScopeSpanCodeObjectId !== scopeSpanCodeObjectId + ) { + setIsPopupOpen(false); + + discardChanges(); + } + }, [ + environmentId, + scopeSpanCodeObjectId, + previousEnvironmentId, + previousScopeSpanCodeObjectId, + discardChanges + ]); + const handleServicesChange = (value: string | string[]) => { sendUserActionTrackingEvent( trackingEvents.GLOBAL_ERRORS_VIEW_SERVICES_FILTER_CHANGED @@ -399,7 +452,35 @@ export const GlobalErrorsFilters = () => { : []) ]; - const applyFilters = () => { + const handleClose = () => { + sendUserActionTrackingEvent( + trackingEvents.GLOBAL_ERRORS_VIEW_FILTERS_CLOSE_BUTTON_CLICKED + ); + + setIsPopupOpen(false); + + discardChanges(); + }; + + const handleFiltersButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.GLOBAL_ERRORS_VIEW_FILTERS_BUTTON_CLICKED + ); + + setIsPopupOpen(!isPopupOpen); + + if (isPopupOpen) { + discardChanges(); + } + }; + + const handleApply = () => { + sendUserActionTrackingEvent( + trackingEvents.GLOBAL_ERRORS_VIEW_FILTERS_APPLY_FILTERS_BUTTON_CLICKED + ); + + setIsPopupOpen(false); + setGlobalErrorsSelectedFilters({ ...globalErrorsSelectedFilters, services: selectedServices, @@ -408,13 +489,7 @@ export const GlobalErrorsFilters = () => { criticalities: selectedCriticalities, handlingTypes: selectedHandlingTypes }); - }; - - const handleClose = () => { - sendUserActionTrackingEvent( - trackingEvents.GLOBAL_ERRORS_VIEW_FILTERS_CLOSE_BUTTON_CLICKED - ); - applyFilters(); + setGloballySelectedServices(selectedServices); }; const handleClearAll = () => { @@ -436,20 +511,25 @@ export const GlobalErrorsFilters = () => { selectedHandlingTypes.length ].filter((x) => x > 0).length; - const handlePopupOpenStateChange = (isOpen: boolean) => { - if (!isOpen) { - applyFilters(); - } - }; + const appliedFiltersCount = [ + (globallySelectedServices ?? []).length, + globalErrorsSelectedFilters.endpoints.length, + globalErrorsSelectedFilters.errorTypes.length, + globalErrorsSelectedFilters.criticalities.length, + globalErrorsSelectedFilters.handlingTypes.length + ].filter((x) => x > 0).length; return ( ); }; diff --git a/src/components/Errors/NewErrorCard/index.tsx b/src/components/Errors/NewErrorCard/index.tsx index d6ab8f254..7f566763e 100644 --- a/src/components/Errors/NewErrorCard/index.tsx +++ b/src/components/Errors/NewErrorCard/index.tsx @@ -310,7 +310,7 @@ export const NewErrorCard = ({ {isDismissEnabled && ( = { - title: "Insights/Issues/IssuesFilter", + title: "Insights/Issues/InsightsCatalog/FilterPanel/IssuesFilter", component: IssuesFilter, parameters: { // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout diff --git a/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/index.tsx b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/index.tsx new file mode 100644 index 000000000..6e337b335 --- /dev/null +++ b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/index.tsx @@ -0,0 +1,373 @@ +import { useCallback, useEffect, useState } from "react"; +import { getFeatureFlagValue } from "../../../../../featureFlags"; +import { usePrevious } from "../../../../../hooks/usePrevious"; +import { useConfigSelector } from "../../../../../store/config/useConfigSelector"; +import { useInsightsSelector } from "../../../../../store/insights/useInsightsSelector"; +import { useStore } from "../../../../../store/useStore"; +import { FeatureFlag } from "../../../../../types"; +import { sendUserActionTrackingEvent } from "../../../../../utils/actions/sendUserActionTrackingEvent"; +import { getInsightTypeInfo } from "../../../../../utils/getInsightTypeInfo"; +import { FilterPopup } from "../../../../common/FilterPopup"; +import { EyeIcon } from "../../../../common/icons/12px/EyeIcon"; +import { FourPointedStarIcon } from "../../../../common/icons/12px/FourPointedStarIcon"; +import { WrenchIcon } from "../../../../common/icons/12px/WrenchIcon"; +import { WarningTriangleIcon } from "../../../../common/icons/WarningTriangleIcon"; +import { IconProps } from "../../../../common/icons/types"; +import { SelectItem } from "../../../../common/v3/Select/types"; +import { useIssuesFilters } from "../../../Issues/useIssuesFilters"; +import { InsightFilterType, ViewMode } from "../../types"; +import * as s from "./styles"; +import { trackingEvents } from "./tracking"; + +export const IssuesFilter = () => { + const [isPopupOpen, setIsPopupOpen] = useState(false); + const { filteredInsightTypes, filters, viewMode, search } = + useInsightsSelector(); + const { + selectedServices: globallySelectedServices, + backendInfo, + scope, + environment + } = useConfigSelector(); + const { + setSelectedServices: setGloballySelectedServices, + setInsightsFilteredInsightTypes: setFilteredInsightTypes, + setInsightsFilters: setFilters + } = useStore.getState(); + const [isCriticalOnly, setIsCriticalOnly] = useState( + filters.includes("criticality") + ); + const [isUnreadOnly, setIsUnreadOnly] = useState( + filters.includes("unread") + ); + const { data, getData } = useIssuesFilters(); + const previousData = usePrevious(data); + const environmentId = environment?.id; + const previousEnvironmentId = usePrevious(environmentId); + const scopeSpanCodeObjectId = scope?.span?.spanCodeObjectId; + const previousScopeSpanCodeObjectId = usePrevious(scopeSpanCodeObjectId); + const isServicesFilterEnabled = + Boolean( + getFeatureFlagValue(backendInfo, FeatureFlag.ARE_ISSUES_FILTERS_ENABLED) + ) && !scopeSpanCodeObjectId; + const [selectedServices, setSelectedServices] = useState( + globallySelectedServices ?? [] + ); + const [selectedInsightTypes, setSelectedInsightTypes] = + useState(filteredInsightTypes); + + // Update selected filters when data is fetched + useEffect(() => { + if (previousData && previousData !== data) { + if (selectedInsightTypes.length > 0) { + const newSelection = selectedInsightTypes.filter((e) => + Boolean(data?.issueTypeFilters.find((x) => x.name === e && x.enabled)) + ); + + if (newSelection.length !== selectedInsightTypes.length) { + setSelectedInsightTypes(newSelection); + } + } + + if (isServicesFilterEnabled && selectedServices.length > 0) { + const newSelection = selectedServices.filter((e) => + Boolean(data?.services?.find((x) => x === e)) + ); + + if (newSelection.length !== selectedServices.length) { + setSelectedServices(newSelection); + } + } + } + }, [ + previousData, + data, + selectedInsightTypes, + setSelectedInsightTypes, + filters, + setSelectedServices, + selectedServices, + isServicesFilterEnabled + ]); + + const discardChanges = useCallback(() => { + const newServices = globallySelectedServices ?? []; + setSelectedServices(newServices); + setSelectedInsightTypes(filteredInsightTypes); + setIsCriticalOnly(filters.includes("criticality")); + setIsUnreadOnly(filters.includes("unread")); + + getData({ + displayName: search, + showDismissed: ViewMode.OnlyDismissed === viewMode, + filters: [ + ...(filters.includes("criticality") ? ["criticality"] : []), + ...(filters.includes("unread") ? ["unread"] : []) + ] as InsightFilterType[], + insightTypes: filteredInsightTypes, + services: newServices, + scopedSpanCodeObjectId: scopeSpanCodeObjectId + }); + }, [ + filteredInsightTypes, + filters, + globallySelectedServices, + search, + scopeSpanCodeObjectId, + viewMode, + getData + ]); + + // Close popup and discard changes on environment or scope changes + useEffect(() => { + if ( + previousEnvironmentId !== environmentId || + previousScopeSpanCodeObjectId !== scopeSpanCodeObjectId + ) { + setIsPopupOpen(false); + discardChanges(); + } + }, [ + discardChanges, + environmentId, + previousEnvironmentId, + previousScopeSpanCodeObjectId, + scopeSpanCodeObjectId + ]); + + const handleApplyFiltersButtonClick = () => { + sendUserActionTrackingEvent(trackingEvents.APPLY_FILTERS_BUTTON_CLICKED); + + setIsPopupOpen(false); + + setFilteredInsightTypes(selectedInsightTypes); + setFilters([ + ...(isCriticalOnly ? ["criticality"] : []), + ...(isUnreadOnly ? ["unread"] : []) + ] as InsightFilterType[]); + if (isServicesFilterEnabled) { + setGloballySelectedServices(selectedServices); + } + }; + + const handleClearFiltersButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.CLEAR_ALL_FILTERS_BUTTON_CLICKED + ); + + setSelectedServices([]); + setSelectedInsightTypes([]); + setIsCriticalOnly(false); + setIsUnreadOnly(false); + }; + + const handleIssueTypesChange = (value: string | string[]) => { + sendUserActionTrackingEvent(trackingEvents.FILTER_OPTION_SELECTED, { + filterType: "issueType" + }); + const newInsightTypes = Array.isArray(value) ? value : [value]; + setSelectedInsightTypes(newInsightTypes); + }; + + const handleServiceChange = (value: string | string[]) => { + sendUserActionTrackingEvent(trackingEvents.FILTER_OPTION_SELECTED, { + filterType: "service" + }); + const newFilteredServices = Array.isArray(value) ? value : [value]; + setSelectedServices(newFilteredServices); + }; + + const handleCloseButtonClick = () => { + sendUserActionTrackingEvent(trackingEvents.CLOSE_FILTER_DIALOG_CLICKED); + + setIsPopupOpen(false); + discardChanges(); + }; + + const handleFiltersButtonClick = () => { + sendUserActionTrackingEvent(trackingEvents.FILTERS_BUTTON_CLICKED); + setIsPopupOpen(!isPopupOpen); + + if (isPopupOpen) { + discardChanges(); + } + }; + + const handleToggleFilterChange = ( + value: string | string[], + filterType: InsightFilterType + ) => { + sendUserActionTrackingEvent(trackingEvents.FILTER_OPTION_SELECTED, { + filterType + }); + + if (filterType === "criticality") { + setIsCriticalOnly(value === filterType); + } + + if (filterType === "unread") { + setIsUnreadOnly(value === filterType); + } + + const selectedFilters = new Set(filters); + if (value === filterType) { + selectedFilters.add(filterType); + } else { + selectedFilters.delete(filterType); + } + }; + + const servicesFilterOptions: SelectItem[] = + data?.services?.map((entry) => ({ + value: entry, + label: entry, + enabled: true, + selected: selectedServices.includes(entry) + })) ?? []; + + const issueTypesFilterOptions: SelectItem[] = + data?.issueTypeFilters?.map((entry) => ({ + value: entry.name, + label: getInsightTypeInfo(entry.name)?.label ?? entry.name, + enabled: entry.enabled || selectedInsightTypes.includes(entry.name), + selected: selectedInsightTypes.includes(entry.name) && entry.enabled + })) ?? []; + + const criticalityFilterOptions: SelectItem[] = [ + { + label: "Critical", + value: "criticality", + enabled: true, + selected: isCriticalOnly + }, + { + label: "All", + value: "all", + enabled: true, + selected: !isCriticalOnly + } + ]; + + const readStatusFilterOptions: SelectItem[] = [ + { + label: "Unread", + value: "unread", + enabled: true, + selected: isUnreadOnly + }, + { + label: "All", + value: "all", + enabled: true, + selected: !isUnreadOnly + } + ]; + + const criticalityFilterPlaceholder = + criticalityFilterOptions.find((x) => x.selected)?.label ?? "All"; + const readStatusFilterPlaceholder = + readStatusFilterOptions.find((x) => x.selected)?.label ?? "All"; + + const selectedFiltersCount = + selectedInsightTypes.length + + (isServicesFilterEnabled ? selectedServices.length : 0) + + (isCriticalOnly ? 1 : 0) + + (isUnreadOnly ? 1 : 0); + + const appliedFiltersCount = + filteredInsightTypes.length + + (isServicesFilterEnabled ? (globallySelectedServices ?? []).length : 0) + + (filters.includes("criticality") ? 1 : 0) + + (filters.includes("unread") ? 1 : 0); + + const filterComponents = [ + ...(isServicesFilterEnabled + ? [ + { + title: "Services", + component: ( + 0 ? "Services" : "All"} + multiselect={true} + icon={(props: IconProps) => ( + + + + )} + disabled={issueTypesFilterOptions?.length === 0} + /> + ) + } + ] + : []), + { + title: "Issues", + component: ( + 0 ? "Issues" : "All"} + multiselect={true} + icon={(props: IconProps) => ( + + + + )} + disabled={issueTypesFilterOptions?.length === 0} + /> + ) + }, + { + title: "Criticality", + component: ( + handleToggleFilterChange(value, "criticality")} + placeholder={criticalityFilterPlaceholder} + icon={(props: IconProps) => ( + + + + )} + showSelectedState={isCriticalOnly} + /> + ) + }, + { + title: "Read/Unread", + component: ( + handleToggleFilterChange(value, "unread")} + placeholder={readStatusFilterPlaceholder} + icon={(props: IconProps) => ( + + + + )} + showSelectedState={isUnreadOnly} + /> + ) + } + ]; + + return ( + + ); +}; diff --git a/src/components/Insights/Issues/IssuesFilter/styles.ts b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/styles.ts similarity index 83% rename from src/components/Insights/Issues/IssuesFilter/styles.ts rename to src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/styles.ts index fc70c432c..0bfe3a858 100644 --- a/src/components/Insights/Issues/IssuesFilter/styles.ts +++ b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/styles.ts @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { Select } from "../../../common/v3/Select"; +import { Select } from "../../../../common/v3/Select"; export const InsightIconContainer = styled.div` display: flex; diff --git a/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/tracking.ts b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/tracking.ts new file mode 100644 index 000000000..4bf92e6c1 --- /dev/null +++ b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/tracking.ts @@ -0,0 +1,15 @@ +import { addPrefix } from "../../../../../utils/addPrefix"; + +const TRACKING_PREFIX = "issues"; + +export const trackingEvents = addPrefix( + TRACKING_PREFIX, + { + FILTER_OPTION_SELECTED: "filter option selected", + APPLY_FILTERS_BUTTON_CLICKED: "apply filters button clicked", + CLEAR_ALL_FILTERS_BUTTON_CLICKED: "clear all filters button clicked", + CLOSE_FILTER_DIALOG_CLICKED: "close filter dialog clicked", + FILTERS_BUTTON_CLICKED: "filters button clicked" + }, + " " +); diff --git a/src/components/Insights/Issues/IssuesFilter/types.ts b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/types.ts similarity index 82% rename from src/components/Insights/Issues/IssuesFilter/types.ts rename to src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/types.ts index 24d48451a..9074f0e68 100644 --- a/src/components/Insights/Issues/IssuesFilter/types.ts +++ b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/types.ts @@ -1,4 +1,4 @@ -import { InsightFilterType } from "../../InsightsCatalog/types"; +import { InsightFilterType } from "../../types"; export interface IssuesFilterEntry { enabled: boolean; 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 3f0170742..cd753410c 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/index.tsx +++ b/src/components/Insights/InsightsCatalog/InsightsPage/insightCards/common/InsightCard/index.tsx @@ -11,7 +11,7 @@ import { TraceIcon } from "../../../../../../common/icons/12px/TraceIcon"; import { DoubleCircleIcon } from "../../../../../../common/icons/16px/DoubleCircleIcon"; import { HistogramIcon } from "../../../../../../common/icons/16px/HistogramIcon"; import { PinIcon } from "../../../../../../common/icons/16px/PinIcon"; -import { QuestionMark } from "../../../../../../common/icons/16px/QuestionMark"; +import { QuestionMarkIcon } from "../../../../../../common/icons/16px/QuestionMarkIcon"; import { RecheckIcon } from "../../../../../../common/icons/16px/RecheckIcon"; import { JiraButton } from "../../../../../../common/v3/JiraButton"; import { Tooltip } from "../../../../../../common/v3/Tooltip"; @@ -325,7 +325,7 @@ export const InsightCard = ({ }} > { setShowInfo(!showInfo); @@ -437,7 +437,7 @@ export const InsightCard = ({ { - const { filteredInsightTypes, filters } = useInsightsSelector(); - const { selectedServices, backendInfo, scope } = useConfigSelector(); - const { - setSelectedServices, - setInsightsFilteredInsightTypes: setFilteredInsightTypes, - setInsightsFilters: setFilters - } = useStore.getState(); - const isCriticalOnly = useMemo( - () => filters.includes("criticality"), - [filters] - ); - const isUnreadOnly = useMemo(() => filters.includes("unread"), [filters]); - const { data } = useIssuesFilters(); - const previousData = usePrevious(data); - const scopeSpanCodeObjectId = scope?.span?.spanCodeObjectId; - const isServicesFilterEnabled = - Boolean( - getFeatureFlagValue(backendInfo, FeatureFlag.ARE_ISSUES_FILTERS_ENABLED) - ) && !scopeSpanCodeObjectId; - const filteredServices = useMemo( - () => selectedServices ?? [], - [selectedServices] - ); - - useEffect(() => { - if (previousData && previousData !== data) { - if (filteredInsightTypes.length > 0) { - const newSelection = filteredInsightTypes.filter((e) => - Boolean(data?.issueTypeFilters.find((x) => x.name === e && x.enabled)) - ); - - if (newSelection.length !== filteredInsightTypes.length) { - setFilteredInsightTypes(newSelection); - } - } - - if (isServicesFilterEnabled && filteredServices.length > 0) { - const newSelection = filteredServices.filter((e) => - Boolean(data?.services?.find((x) => x === e)) - ); - - if (newSelection.length !== filteredServices.length) { - setSelectedServices(newSelection); - } - } - } - }, [ - previousData, - data, - filteredInsightTypes, - setFilteredInsightTypes, - filters, - setSelectedServices, - filteredServices, - isServicesFilterEnabled - ]); - - const handleClearFiltersButtonClick = () => { - sendUserActionTrackingEvent( - trackingEvents.CLEAR_ALL_FILTERS_BUTTON_CLICKED - ); - setFilteredInsightTypes([]); - setFilters([]); - - if (isServicesFilterEnabled) { - setSelectedServices([]); - } - }; - - const handleIssueTypesChange = (value: string | string[]) => { - sendUserActionTrackingEvent(trackingEvents.FILTER_OPTION_SELECTED, { - filterType: "issueType" - }); - const newInsightTypes = Array.isArray(value) ? value : [value]; - setFilteredInsightTypes(newInsightTypes); - }; - - const handleServiceChange = (value: string | string[]) => { - sendUserActionTrackingEvent(trackingEvents.FILTER_OPTION_SELECTED, { - filterType: "service" - }); - const newFilteredServices = Array.isArray(value) ? value : [value]; - setSelectedServices(newFilteredServices); - }; - - const handleCloseButtonClick = () => { - sendUserActionTrackingEvent(trackingEvents.CLOSE_FILTER_DIALOG_CLICKED); - }; - - const handleToggleFilterChange = ( - value: string | string[], - filterType: InsightFilterType - ) => { - sendUserActionTrackingEvent(trackingEvents.FILTER_OPTION_SELECTED, { - filterType - }); - const selectedFilters = new Set(filters); - if (value === filterType) { - selectedFilters.add(filterType); - } else { - selectedFilters.delete(filterType); - } - - setFilters(Array.from(selectedFilters)); - }; - - const servicesFilterOptions: SelectItem[] = - data?.services?.map((entry) => ({ - value: entry, - label: entry, - enabled: true, - selected: filteredServices.includes(entry) - })) ?? []; - - const issueTypesFilterOptions: SelectItem[] = - data?.issueTypeFilters?.map((entry) => ({ - value: entry.name, - label: getInsightTypeInfo(entry.name)?.label ?? entry.name, - enabled: entry.enabled || filteredInsightTypes.includes(entry.name), - selected: filteredInsightTypes.includes(entry.name) && entry.enabled - })) ?? []; - - const criticalityFilterOptions: SelectItem[] = [ - { - label: "Critical", - value: "criticality", - enabled: true, - selected: isCriticalOnly - }, - { - label: "All", - value: "all", - enabled: true, - selected: !isCriticalOnly - } - ]; - - const readStatusFilterOptions: SelectItem[] = [ - { - label: "Unread", - value: "unread", - enabled: true, - selected: isUnreadOnly - }, - { - label: "All", - value: "all", - enabled: true, - selected: !isUnreadOnly - } - ]; - - const criticalityFilterPlaceholder = - criticalityFilterOptions.find((x) => x.selected)?.label ?? "All"; - const readStatusFilterPlaceholder = - readStatusFilterOptions.find((x) => x.selected)?.label ?? "All"; - const selectedFiltersCount = - filteredInsightTypes.length + - (isServicesFilterEnabled ? filteredServices.length : 0) + - (isCriticalOnly ? 1 : 0) + - (isUnreadOnly ? 1 : 0); - - const filterComponents = [ - { - title: "Issues", - component: ( - 0 ? "Issues" : "All"} - multiselect={true} - icon={(props: IconProps) => ( - - - - )} - disabled={issueTypesFilterOptions?.length === 0} - /> - ) - }, - { - title: "Criticality", - component: ( - handleToggleFilterChange(value, "criticality")} - placeholder={criticalityFilterPlaceholder} - icon={(props: IconProps) => ( - - - - )} - showSelectedState={isCriticalOnly} - /> - ) - }, - { - title: "Read/Unread", - component: ( - handleToggleFilterChange(value, "unread")} - placeholder={readStatusFilterPlaceholder} - icon={(props: IconProps) => ( - - - - )} - showSelectedState={isUnreadOnly} - /> - ) - } - ]; - - if (isServicesFilterEnabled) { - filterComponents.unshift({ - title: "Services", - component: ( - 0 ? "Services" : "All"} - multiselect={true} - icon={(props: IconProps) => ( - - - - )} - disabled={issueTypesFilterOptions?.length === 0} - /> - ) - }); - } - - return ( - - ); -}; diff --git a/src/components/Insights/Issues/IssuesFilter/tracking.ts b/src/components/Insights/Issues/IssuesFilter/tracking.ts deleted file mode 100644 index 32ef8f6d2..000000000 --- a/src/components/Insights/Issues/IssuesFilter/tracking.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { addPrefix } from "../../../../utils/addPrefix"; - -const TRACKING_PREFIX = "issues"; - -export const trackingEvents = addPrefix( - TRACKING_PREFIX, - { - FILTER_OPTION_SELECTED: "filter option selected", - CLEAR_ALL_FILTERS_BUTTON_CLICKED: "clear all filters button clicked", - CLOSE_FILTER_DIALOG_CLICKED: "close filter dialog clicked" - }, - " " -); diff --git a/src/components/Insights/Issues/useIssuesFilters.ts b/src/components/Insights/Issues/useIssuesFilters.ts index 5b174314a..aa7223f51 100644 --- a/src/components/Insights/Issues/useIssuesFilters.ts +++ b/src/components/Insights/Issues/useIssuesFilters.ts @@ -6,9 +6,9 @@ import { useConfigSelector } from "../../../store/config/useConfigSelector"; import { useInsightsSelector } from "../../../store/insights/useInsightsSelector"; import { useStore } from "../../../store/useStore"; import { GetIssuesFiltersPayload } from "../../../types"; +import { IssuesFiltersData } from "../InsightsCatalog/FilterPanel/IssuesFilter/types"; import { ViewMode } from "../InsightsCatalog/types"; import { actions as issuesActions } from "./actions"; -import { IssuesFiltersData } from "./IssuesFilter/types"; import { GetIssuesFiltersQuery } from "./types"; const REFRESH_INTERVAL = 10 * 1000; // in milliseconds @@ -103,6 +103,7 @@ export const useIssuesFilters = () => { return { data, - refresh + refresh, + getData: getFilters }; }; diff --git a/src/components/Insights/index.tsx b/src/components/Insights/index.tsx index eeb11aaf4..781d90353 100644 --- a/src/components/Insights/index.tsx +++ b/src/components/Insights/index.tsx @@ -22,7 +22,7 @@ import { LightBulbSmallIcon } from "../common/icons/LightBulbSmallIcon"; import { OpenTelemetryLogoCrossedSmallIcon } from "../common/icons/OpenTelemetryLogoCrossedSmallIcon"; import { SlackLogoIcon } from "../common/icons/SlackLogoIcon"; import { InsightsCatalog } from "./InsightsCatalog"; -import { IssuesFilterQuery } from "./Issues/IssuesFilter/types"; +import { IssuesFilterQuery } from "./InsightsCatalog/FilterPanel/IssuesFilter/types"; import { EndpointBottleneckInsightTicket } from "./insightTickets/EndpointBottleneckInsightTicket"; import { EndpointHighNumberOfQueriesInsightTicket } from "./insightTickets/EndpointHighNumberOfQueriesInsightTicket"; import { EndpointQueryOptimizationV2InsightTicket } from "./insightTickets/EndpointQueryOptimizationV2InsightTicket"; diff --git a/src/components/Navigation/ScopeBar/LinkedEndpointsMenu/index.tsx b/src/components/Navigation/ScopeBar/LinkedEndpointsMenu/index.tsx index 01fce27f9..4a46a0e20 100644 --- a/src/components/Navigation/ScopeBar/LinkedEndpointsMenu/index.tsx +++ b/src/components/Navigation/ScopeBar/LinkedEndpointsMenu/index.tsx @@ -23,7 +23,7 @@ export const LinkedEndpointsMenu = ({ id: x.spanCodeObjectId, customContent: ( handleMenuItemClick(x)}> - + {x.displayName} ), diff --git a/src/components/common/FilterButton/index.tsx b/src/components/common/FilterButton/index.tsx index b7a51f3fa..41a31a9c8 100644 --- a/src/components/common/FilterButton/index.tsx +++ b/src/components/common/FilterButton/index.tsx @@ -8,11 +8,13 @@ export const FilterButton = ({ title, showCount, isLoading, - isActive + isActive, + onClick }: FilterButtonProps) => ( 0} $isActive={isActive} + onClick={onClick} > {title} diff --git a/src/components/common/FilterButton/types.ts b/src/components/common/FilterButton/types.ts index 76a48a685..e5c0b5d13 100644 --- a/src/components/common/FilterButton/types.ts +++ b/src/components/common/FilterButton/types.ts @@ -4,6 +4,7 @@ export interface FilterButtonProps { selectedCount?: number; showCount?: boolean; isActive: boolean; + onClick?: () => void; } export interface ButtonProps { diff --git a/src/components/common/FilterPopup/index.tsx b/src/components/common/FilterPopup/index.tsx index 782841fed..852e4d59b 100644 --- a/src/components/common/FilterPopup/index.tsx +++ b/src/components/common/FilterPopup/index.tsx @@ -1,8 +1,7 @@ -import { useEffect, useState } from "react"; -import { usePrevious } from "../../../hooks/usePrevious"; import { FilterButton } from "../FilterButton"; import { CrossIcon } from "../icons/16px/CrossIcon"; import { NewPopover } from "../NewPopover"; +import { NewButton } from "../v3/NewButton"; import * as s from "./styles"; import { FilterPopupProps } from "./types"; @@ -11,26 +10,28 @@ export const FilterPopup = ({ onClose, title, selectedFiltersCount, + appliedFiltersCount, filters, - onStateChange + onApply, + isOpen, + onFiltersButtonClick }: FilterPopupProps) => { - const [isOpen, setIsOpen] = useState(false); - const previousIsOpen = usePrevious(isOpen); - - useEffect(() => { - if (isOpen !== previousIsOpen && onStateChange) { - onStateChange(isOpen); - } - }, [isOpen, previousIsOpen, onStateChange]); + const handleApplyButtonClick = () => { + onApply(); + }; const handleCloseButtonClick = () => { - setIsOpen(false); onClose(); }; + const handleFilterButtonClick = () => { + onFiltersButtonClick(); + }; + return ( @@ -39,7 +40,6 @@ export const FilterPopup = ({ - {filters.map((x) => ( {x.title} @@ -47,6 +47,10 @@ export const FilterPopup = ({ ))} + } - onOpenChange={setIsOpen} isOpen={isOpen} placement={"bottom-end"} > @@ -64,8 +67,9 @@ export const FilterPopup = ({ diff --git a/src/components/common/FilterPopup/types.ts b/src/components/common/FilterPopup/types.ts index 259210b2a..8a898f411 100644 --- a/src/components/common/FilterPopup/types.ts +++ b/src/components/common/FilterPopup/types.ts @@ -1,13 +1,16 @@ import { ReactNode } from "react"; export interface FilterPopupProps { + onApply: () => void; onClose: () => void; + onFiltersButtonClick: () => void; title: string; onClearAll: () => void; selectedFiltersCount: number; + appliedFiltersCount: number; filters: { title: string; component: ReactNode; }[]; - onStateChange?: (state: boolean) => void; + isOpen: boolean; } diff --git a/src/components/common/Loader/index.tsx b/src/components/common/Loader/index.tsx index 3ed91b6fb..c6e6c5316 100644 --- a/src/components/common/Loader/index.tsx +++ b/src/components/common/Loader/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-curly-brace-presence */ import React from "react"; import { CircleLoader } from "../CircleLoader"; import { LoaderProps } from "./types"; diff --git a/src/components/common/NewCircleLoader/index.tsx b/src/components/common/NewCircleLoader/index.tsx index d5173b7de..20a31c319 100644 --- a/src/components/common/NewCircleLoader/index.tsx +++ b/src/components/common/NewCircleLoader/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-curly-brace-presence */ // Source: https://www.benmvp.com/blog/how-to-create-circle-svg-gradient-loading-spinner/ import { DefaultTheme, useTheme } from "styled-components"; import { DEFAULT_ICON_SIZE } from "../icons/hooks"; diff --git a/src/components/common/NewPopover/index.tsx b/src/components/common/NewPopover/index.tsx index 0e34cb0df..67288d387 100644 --- a/src/components/common/NewPopover/index.tsx +++ b/src/components/common/NewPopover/index.tsx @@ -74,7 +74,8 @@ export const NewPopover = ({ children, sameWidth, width, - content + content, + closeOnOutsidePress }: PopoverProps) => { const arrowRef = useRef(null); const theme = useTheme(); @@ -102,7 +103,9 @@ export const NewPopover = ({ }); const click = useClick(context); - const dismiss = useDismiss(context); + const dismiss = useDismiss(context, { + outsidePress: closeOnOutsidePress + }); const { getReferenceProps, getFloatingProps } = useInteractions([ ...(useClickInteraction === false ? [] : [click]), diff --git a/src/components/common/NewPopover/types.ts b/src/components/common/NewPopover/types.ts index cb6235754..d6416042c 100644 --- a/src/components/common/NewPopover/types.ts +++ b/src/components/common/NewPopover/types.ts @@ -12,4 +12,5 @@ export interface PopoverProps { width?: number | string; sameWidth?: boolean; useClickInteraction?: boolean; + closeOnOutsidePress?: boolean; } diff --git a/src/components/common/icons/16px/QuestionMark.tsx b/src/components/common/icons/16px/QuestionMarkIcon.tsx similarity index 85% rename from src/components/common/icons/16px/QuestionMark.tsx rename to src/components/common/icons/16px/QuestionMarkIcon.tsx index 1d6fecb05..7aa84291a 100644 --- a/src/components/common/icons/16px/QuestionMark.tsx +++ b/src/components/common/icons/16px/QuestionMarkIcon.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useIconProps } from "../hooks"; import { IconProps } from "../types"; -const QuestionMarkComponent = (props: IconProps) => { +const QuestionMarkIconComponent = (props: IconProps) => { const { size, color } = useIconProps(props); return ( @@ -25,4 +25,4 @@ const QuestionMarkComponent = (props: IconProps) => { ); }; -export const QuestionMark = React.memo(QuestionMarkComponent); +export const QuestionMarkIcon = React.memo(QuestionMarkIconComponent); diff --git a/src/components/common/icons/DigmaLoginLogo.tsx b/src/components/common/icons/DigmaLoginLogo.tsx index 5c448e71c..e5dec6513 100644 --- a/src/components/common/icons/DigmaLoginLogo.tsx +++ b/src/components/common/icons/DigmaLoginLogo.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-curly-brace-presence */ import React from "react"; const DigmaLoginLogoComponent = () => { diff --git a/src/components/common/v3/Select/index.tsx b/src/components/common/v3/Select/index.tsx index 1e6cf8ec0..9eb0322f3 100644 --- a/src/components/common/v3/Select/index.tsx +++ b/src/components/common/v3/Select/index.tsx @@ -49,6 +49,7 @@ export const Select = ({ const handleButtonClick = () => { setIsOpen(!isOpen); + setSearchValue(""); }; const handleItemClick = (item: SelectItem) => { @@ -59,6 +60,7 @@ export const Select = ({ if (!multiselect) { onChange(item.value); setIsOpen(false); + setSearchValue(""); return; } @@ -112,6 +114,7 @@ export const Select = ({ type={"text"} placeholder={"Search"} onChange={handleSearchChange} + value={searchValue} /> )} diff --git a/src/store/assets/assetsSlice.ts b/src/store/assets/assetsSlice.ts index 8545a1f10..5c288f7e4 100644 --- a/src/store/assets/assetsSlice.ts +++ b/src/store/assets/assetsSlice.ts @@ -6,16 +6,26 @@ import { SORTING_ORDER } from "../../components/Assets/AssetList/types"; import { AssetCategoriesData } from "../../components/Assets/AssetTypeList/types"; -import { AssetFilterQuery } from "../../components/Assets/AssetsFilter/types"; import { ViewMode } from "../../components/Assets/AssetsViewScopeConfiguration/types"; +export interface AssetsFilters { + services: string[]; + endpoints: string[]; + consumers: string[]; + internals: string[]; + insights: string[]; + scopedSpanCodeObjectId?: string; + directOnly?: boolean; + displayName?: string; +} + export interface AssetsState { assetCategoriesData: AssetCategoriesData | null; isAssetCategoriesDataLoading: boolean; selectedAssetCategory: string | null; assets: AssetsData | null; areAssetsLoading: boolean; - filters: AssetFilterQuery; + filters: AssetsFilters | null; viewMode: ViewMode; search: string; page: number; @@ -24,17 +34,13 @@ export interface AssetsState { } const allFiltersInitialState: { - filters: AssetFilterQuery; + filters: AssetsFilters | null; viewMode: ViewMode; search: string; page: number; sorting: Sorting; } = { - filters: { - services: [], - operations: [], - insights: [] - }, + filters: null, viewMode: "descendants", search: "", page: 0, @@ -72,7 +78,7 @@ export const assetsSlice = createSlice({ setAssets: (assets: AssetsData) => set({ assets }), setAreAssetsLoading: (isLoading: boolean) => set({ areAssetsLoading: isLoading }), - setAssetsFilters: (filters: AssetFilterQuery) => set({ filters }), + setAssetsFilters: (filters: AssetsFilters) => set({ filters }), setAssetsViewMode: (viewMode: ViewMode) => set({ viewMode }), setAssetsSearch: (search: string) => set({ search }), setAssetsPage: (page: number) => set({ page }), diff --git a/src/store/insights/insightsSlice.ts b/src/store/insights/insightsSlice.ts index d8ed3c787..2147cd2c2 100644 --- a/src/store/insights/insightsSlice.ts +++ b/src/store/insights/insightsSlice.ts @@ -3,12 +3,12 @@ import { Sorting, SORTING_ORDER } from "../../components/common/SortingSelector/types"; +import { IssuesFiltersData } from "../../components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/types"; import { InsightFilterType, SORTING_CRITERION, ViewMode } from "../../components/Insights/InsightsCatalog/types"; -import { IssuesFiltersData } from "../../components/Insights/Issues/IssuesFilter/types"; import { InsightsData, InsightViewType } from "../../components/Insights/types"; interface InsightsState {