diff --git a/src/components/Assets/AssetList/index.tsx b/src/components/Assets/AssetList/index.tsx index a005a3a20..8dc213650 100644 --- a/src/components/Assets/AssetList/index.tsx +++ b/src/components/Assets/AssetList/index.tsx @@ -129,7 +129,12 @@ export const AssetList = ({ ); const listRef = useRef(null); - const { environment, backendInfo, scope } = useConfigSelector(); + const { + environment, + backendInfo, + scope, + selectedServices: globallySelectedServices + } = useConfigSelector(); const scopeSpanCodeObjectId = scope?.span?.spanCodeObjectId; const isServicesFilterEnabled = !scopeSpanCodeObjectId; const isInitialLoading = !data; @@ -150,7 +155,7 @@ export const AssetList = ({ ...(filters?.consumers ?? []), ...(filters?.internals ?? []) ], - services: scopeSpanCodeObjectId ? [] : filters?.services ?? [], + services: scopeSpanCodeObjectId ? [] : globallySelectedServices ?? [], directOnly: viewMode === "children" } }), @@ -158,6 +163,7 @@ export const AssetList = ({ page, assetTypeId, filters, + globallySelectedServices, viewMode, scopeSpanCodeObjectId, search, @@ -198,7 +204,8 @@ export const AssetList = ({ const areAnyFiltersApplied = checkIfAnyFiltersApplied( filters, search, - isServicesFilterEnabled + isServicesFilterEnabled, + globallySelectedServices ); useEffect(() => { diff --git a/src/components/Assets/AssetTypeList/index.tsx b/src/components/Assets/AssetTypeList/index.tsx index 9b1dfb11a..73b98c0a0 100644 --- a/src/components/Assets/AssetTypeList/index.tsx +++ b/src/components/Assets/AssetTypeList/index.tsx @@ -42,6 +42,7 @@ export const ASSET_TYPE_IDS = [ const getData = ( filters: AssetsFilters | null, + globallySelectedServices: string[] | null, searchQuery: string, viewMode: ViewMode, scopeSpanCodeObjectId?: string @@ -59,7 +60,7 @@ const getData = ( ...(filters?.consumers ?? []), ...(filters?.internals ?? []) ], - services: scopeSpanCodeObjectId ? [] : filters?.services ?? [] + services: scopeSpanCodeObjectId ? [] : globallySelectedServices ?? [] }, ...(searchQuery.length > 0 ? { displayName: searchQuery } : {}) } @@ -84,7 +85,11 @@ export const AssetTypeList = ({ const previousData = usePrevious(data); const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); const previousLastSetDataTimeStamp = usePrevious(lastSetDataTimeStamp); - const { scope, environment } = useConfigSelector(); + const { + scope, + environment, + selectedServices: globallySelectedServices + } = useConfigSelector(); const scopeSpanCodeObjectId = scope?.span?.spanCodeObjectId; const previousScopeSpanCodeObjectId = usePrevious(scopeSpanCodeObjectId); const previousEnvironment = usePrevious(environment); @@ -95,8 +100,15 @@ export const AssetTypeList = ({ const isInitialLoading = !data; const refreshData = useCallback( - () => getData(filters, search, viewMode, scopeSpanCodeObjectId), - [filters, scopeSpanCodeObjectId, viewMode, search] + () => + getData( + filters, + globallySelectedServices, + search, + viewMode, + scopeSpanCodeObjectId + ), + [filters, globallySelectedServices, scopeSpanCodeObjectId, viewMode, search] ); useEffect(() => { @@ -106,7 +118,8 @@ export const AssetTypeList = ({ const areAnyFiltersApplied = checkIfAnyFiltersApplied( filters, search, - isServicesFilterEnabled + isServicesFilterEnabled, + globallySelectedServices ); useEffect(() => { diff --git a/src/components/Assets/AssetsFilter/index.tsx b/src/components/Assets/AssetsFilter/index.tsx index d94b03bff..1b5969ae6 100644 --- a/src/components/Assets/AssetsFilter/index.tsx +++ b/src/components/Assets/AssetsFilter/index.tsx @@ -240,7 +240,7 @@ export const AssetsFilter = () => { ]); const discardChanges = useCallback(() => { - setSelectedServices(filters?.services ?? []); + setSelectedServices(globallySelectedServices ?? []); setSelectedEndpoints(filters?.endpoints ?? []); setSelectedConsumers(filters?.consumers ?? []); setSelectedInternals(filters?.internals ?? []); @@ -248,7 +248,7 @@ export const AssetsFilter = () => { getData({ ...query, - services: filters?.services ?? [], + services: globallySelectedServices ?? [], operations: [ ...(filters?.endpoints ?? []), ...(filters?.consumers ?? []), @@ -256,7 +256,7 @@ export const AssetsFilter = () => { ], insights: (filters?.insights as InsightType[]) ?? [] }); - }, [filters, query]); + }, [globallySelectedServices, filters, query]); // Close popup on environment or scope changes useEffect(() => { @@ -423,7 +423,7 @@ export const AssetsFilter = () => { const appliedFiltersCount = filters ? [ - ...(isServicesFilterEnabled ? filters.services : []), + ...(isServicesFilterEnabled ? globallySelectedServices ?? [] : []), ...filters.endpoints, ...filters.consumers, ...filters.internals, diff --git a/src/components/Assets/index.tsx b/src/components/Assets/index.tsx index 0374f04a1..d0997a421 100644 --- a/src/components/Assets/index.tsx +++ b/src/components/Assets/index.tsx @@ -78,10 +78,17 @@ export const Assets = () => { useEffect(() => { if ( isUndefined(previousPersistedFilters) && - !isUndefined(persistedFilters) && - persistedFilters + !isUndefined(persistedFilters) ) { - setFilters(persistedFilters); + setFilters( + persistedFilters ?? { + services: [], + endpoints: [], + consumers: [], + internals: [], + insights: [] + } + ); } }, [previousPersistedFilters, persistedFilters, setFilters]); @@ -217,7 +224,6 @@ export const Assets = () => { )} - {renderContent()} ); diff --git a/src/components/Assets/utils.tsx b/src/components/Assets/utils.tsx index abc70fda4..110c894ef 100644 --- a/src/components/Assets/utils.tsx +++ b/src/components/Assets/utils.tsx @@ -55,7 +55,8 @@ export const getAssetTypeInfo = ( export const checkIfAnyFiltersApplied = ( filters: AssetsFilters | null, searchQuery: string, - isServicesFilterEnabled: boolean + isServicesFilterEnabled: boolean, + globallySelectedServices: string[] | null ) => Boolean( filters && @@ -64,6 +65,6 @@ export const checkIfAnyFiltersApplied = ( ...filters.endpoints, ...filters.consumers, ...filters.internals, - ...(isServicesFilterEnabled ? filters.services : []) + ...(isServicesFilterEnabled ? globallySelectedServices ?? [] : []) ].length > 0 ) || searchQuery.length > 0; diff --git a/src/components/Dashboard/MetricsReport/Header/index.tsx b/src/components/Dashboard/MetricsReport/Header/index.tsx index 8c86ef6cb..626d7dd2a 100644 --- a/src/components/Dashboard/MetricsReport/Header/index.tsx +++ b/src/components/Dashboard/MetricsReport/Header/index.tsx @@ -311,6 +311,8 @@ export const Header = ({ onGoBack }: HeaderProps) => { selected: selectedEndpoints.includes(x.spanCodeObjectId) })) ?? [] } + useShift={false} + sameWidth={false} showSelectedState={true} multiselect={true} icon={WrenchIcon} diff --git a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/getFiltersOptions.ts b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/getFiltersOptions.ts new file mode 100644 index 000000000..a55f581f4 --- /dev/null +++ b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/getFiltersOptions.ts @@ -0,0 +1,96 @@ +import { fetchData } from "../../../../utils/fetchData"; +import { actions } from "../../actions"; +import { + EndpointFilterData, + GetGlobalErrorsFiltersDataPayload, + SetGlobalErrorsFiltersDataPayload +} from "./types"; + +export interface FiltersOptions { + services: string[]; + endpoints: EndpointFilterData[]; + errorTypes: string[]; +} + +const toFiltersOptions = ( + filtersData: SetGlobalErrorsFiltersDataPayload +): Partial => { + const servicesData = filtersData.filters.find( + (filter) => filter.filterName === "Services" + ); + + const endpointsData = filtersData.filters.find( + (filter) => filter.filterName === "Endpoints" + ); + + const errorTypesData = filtersData.filters.find( + (filter) => filter.filterName === "ErrorTypes" + ); + + return { + ...(servicesData ? { services: servicesData.values as string[] } : {}), + ...(endpointsData + ? { endpoints: endpointsData.values as EndpointFilterData[] } + : {}), + ...(errorTypesData ? { errorTypes: errorTypesData.values as string[] } : {}) + }; +}; + +const fetcherConfig = { + requestAction: actions.GET_GLOBAL_ERRORS_FILTERS_DATA, + responseAction: actions.SET_GLOBAL_ERRORS_FILTERS_DATA +}; + +export const getFiltersOptions = async ( + environmentId: string, + selectedServices: string[], + selectedEndpoints: string[] +): Promise<{ + services: string[]; + endpoints: EndpointFilterData[]; + errorTypes: string[]; +}> => { + const servicesData = await fetchData< + GetGlobalErrorsFiltersDataPayload, + SetGlobalErrorsFiltersDataPayload + >(fetcherConfig, { + environment: environmentId + }); + + const endpointsData = + selectedServices && selectedServices.length > 0 + ? await fetchData< + GetGlobalErrorsFiltersDataPayload, + SetGlobalErrorsFiltersDataPayload + >(fetcherConfig, { + environment: environmentId, + filterName: "Services", + filterData: { + values: selectedServices + } + }) + : undefined; + + const errorTypesData = + selectedEndpoints && selectedEndpoints.length > 0 + ? await fetchData< + GetGlobalErrorsFiltersDataPayload, + SetGlobalErrorsFiltersDataPayload + >(fetcherConfig, { + environment: environmentId, + filterName: "Endpoints", + filterData: { + values: selectedEndpoints, + services: selectedServices + } + }) + : undefined; + + const filtersOptions: FiltersOptions = { + ...(toFiltersOptions(servicesData) as FiltersOptions), + ...(endpointsData ? toFiltersOptions(endpointsData) : {}), + ...(errorTypesData ? toFiltersOptions(errorTypesData) : {}) + }; + + return filtersOptions; +}; diff --git a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx index 6f89a254a..dbcb60269 100644 --- a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx +++ b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx @@ -1,13 +1,10 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { getFeatureFlagValue } from "../../../../featureFlags"; -import { useFetchData } from "../../../../hooks/useFetchData"; import { usePrevious } from "../../../../hooks/usePrevious"; import { useConfigSelector } from "../../../../store/config/useConfigSelector"; import { ErrorCriticality, - ErrorFilter, - ErrorHandlingType, - GlobalErrorsFiltersState + ErrorHandlingType } from "../../../../store/errors/errorsSlice"; import { useErrorsSelector } from "../../../../store/errors/useErrorsSelector"; import { useStore } from "../../../../store/useStore"; @@ -21,20 +18,16 @@ 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 { getFiltersOptions } from "./getFiltersOptions"; import * as s from "./styles"; -import { - EndpointFilterData, - GetGlobalErrorsFiltersDataPayload, - SetGlobalErrorsFiltersDataPayload -} from "./types"; const getSelectPlaceholder = (options: SelectItem[], placeholder: string) => options.filter((x) => x.selected).length > 0 ? placeholder : "All"; export const GlobalErrorsFilters = () => { const [isPopupOpen, setIsPopupOpen] = useState(false); + const previousIsPopupOpen = usePrevious(isPopupOpen); const { environment, backendInfo, @@ -58,122 +51,81 @@ export const GlobalErrorsFilters = () => { const previousEnvironmentId = usePrevious(environmentId); const scopeSpanCodeObjectId = scope?.span?.spanCodeObjectId; const previousScopeSpanCodeObjectId = usePrevious(scopeSpanCodeObjectId); - - const [lastChangedFilter, setLastChangedFilter] = useState< - ErrorFilter | undefined - >(undefined); + const [areFilterOptionsLoaded, setAreFiltersOptionsLoaded] = useState(false); const [selectedServices, setSelectedServices] = useState( globallySelectedServices ?? [] ); const [selectedEndpoints, setSelectedEndpoints] = useState( - globalErrorsSelectedFilters.endpoints + globalErrorsSelectedFilters?.endpoints ?? [] ); const [selectedErrorTypes, setSelectedErrorTypes] = useState( - globalErrorsSelectedFilters.errorTypes + globalErrorsSelectedFilters?.errorTypes ?? [] ); const [selectedCriticalities, setSelectedCriticalities] = useState< ErrorCriticality[] - >(globalErrorsSelectedFilters.criticalities); + >(globalErrorsSelectedFilters?.criticalities ?? []); const [selectedHandlingTypes, setSelectedHandlingTypes] = useState< ErrorHandlingType[] - >(globalErrorsSelectedFilters.handlingTypes); - - const getLastSelectedFilterValues = useCallback( - (changedFilter: ErrorFilter) => { - switch (changedFilter) { - case "Services": - return selectedServices; - case "Endpoints": - return selectedEndpoints; - case "ErrorTypes": - return selectedErrorTypes; - } - }, - [selectedServices, selectedEndpoints, selectedErrorTypes] - ); + >(globalErrorsSelectedFilters?.handlingTypes ?? []); - const payload: GetGlobalErrorsFiltersDataPayload = useMemo( - () => ({ - environment: environmentId ?? "", - ...(lastChangedFilter - ? { - filterName: lastChangedFilter, - filterData: { - values: getLastSelectedFilterValues(lastChangedFilter), - ...(lastChangedFilter === "Endpoints" - ? { services: selectedServices } - : {}) - } - } - : {}) - }), - [ + const getOptions = useCallback(async () => { + if (!environmentId || !isPopupOpen) { + return; + } + + const filterOptions = await getFiltersOptions( environmentId, selectedServices, - getLastSelectedFilterValues, - lastChangedFilter - ] - ); + selectedEndpoints + ); - 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); + setGlobalErrorsFilters(filterOptions); + }, [ + isPopupOpen, + environmentId, + selectedServices, + selectedEndpoints, + setGlobalErrorsFilters + ]); + // Get filter options on popup open 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 (!previousIsPopupOpen && isPopupOpen) { + void getOptions().then(() => { + setAreFiltersOptionsLoaded(true); + }); + } + }, [previousIsPopupOpen, isPopupOpen, getOptions]); - if (newErrorTypes) { - newGlobalErrorsFilters.errorTypes = newErrorTypes.values as string[]; - } + // Get filter options on selected services/endpoints change + useEffect(() => { + void getOptions(); + }, [getOptions]); - setGlobalErrorsFilters(newGlobalErrorsFilters); + // Clear filter options loaded state on popup close + useEffect(() => { + if (!isPopupOpen && previousIsPopupOpen) { + setAreFiltersOptionsLoaded(false); } - }, [previousData, data, setGlobalErrorsFilters, globalErrorsFilters]); + }, [isPopupOpen, previousIsPopupOpen, setGlobalErrorsFilters]); useEffect(() => { - setSelectedServices(globalErrorsSelectedFilters.services); - setSelectedEndpoints(globalErrorsSelectedFilters.endpoints); - setSelectedErrorTypes(globalErrorsSelectedFilters.errorTypes); + setSelectedServices(globallySelectedServices ?? []); + setSelectedEndpoints(globalErrorsSelectedFilters?.endpoints ?? []); + setSelectedErrorTypes(globalErrorsSelectedFilters?.errorTypes ?? []); + setSelectedCriticalities(globalErrorsSelectedFilters?.criticalities ?? []); + setSelectedHandlingTypes(globalErrorsSelectedFilters?.handlingTypes ?? []); }, [ - globalErrorsSelectedFilters.services, - globalErrorsSelectedFilters.endpoints, - globalErrorsSelectedFilters.errorTypes + globallySelectedServices, + globalErrorsSelectedFilters?.endpoints, + globalErrorsSelectedFilters?.errorTypes, + globalErrorsSelectedFilters?.criticalities, + globalErrorsSelectedFilters?.handlingTypes ]); // Clear filters on environment change, but keep the selected services useEffect(() => { - if (previousEnvironmentId !== environmentId) { + if (previousEnvironmentId ?? previousEnvironmentId !== environmentId) { setSelectedEndpoints([]); setSelectedErrorTypes([]); setSelectedCriticalities([]); @@ -182,17 +134,17 @@ export const GlobalErrorsFilters = () => { }, [environmentId, previousEnvironmentId]); const discardChanges = useCallback(() => { - setSelectedServices(globalErrorsSelectedFilters.services); - setSelectedEndpoints(globalErrorsSelectedFilters.endpoints); - setSelectedErrorTypes(globalErrorsSelectedFilters.errorTypes); - setSelectedCriticalities(globalErrorsSelectedFilters.criticalities); - setSelectedHandlingTypes(globalErrorsSelectedFilters.handlingTypes); + setSelectedServices(globallySelectedServices ?? []); + setSelectedEndpoints(globalErrorsSelectedFilters?.endpoints ?? []); + setSelectedErrorTypes(globalErrorsSelectedFilters?.errorTypes ?? []); + setSelectedCriticalities(globalErrorsSelectedFilters?.criticalities ?? []); + setSelectedHandlingTypes(globalErrorsSelectedFilters?.handlingTypes ?? []); }, [ - globalErrorsSelectedFilters.services, - globalErrorsSelectedFilters.endpoints, - globalErrorsSelectedFilters.errorTypes, - globalErrorsSelectedFilters.criticalities, - globalErrorsSelectedFilters.handlingTypes + globallySelectedServices, + globalErrorsSelectedFilters?.endpoints, + globalErrorsSelectedFilters?.errorTypes, + globalErrorsSelectedFilters?.criticalities, + globalErrorsSelectedFilters?.handlingTypes ]); // Close popup on environment or scope changes @@ -218,7 +170,6 @@ export const GlobalErrorsFilters = () => { trackingEvents.GLOBAL_ERRORS_VIEW_SERVICES_FILTER_CHANGED ); const newValue = Array.isArray(value) ? value : [value]; - setLastChangedFilter("Services"); setSelectedServices(newValue); setSelectedEndpoints([]); setSelectedErrorTypes([]); @@ -229,7 +180,6 @@ export const GlobalErrorsFilters = () => { trackingEvents.GLOBAL_ERRORS_VIEW_ENDPOINTS_FILTER_CHANGED ); const newValue = Array.isArray(value) ? value : [value]; - setLastChangedFilter("Endpoints"); setSelectedEndpoints(newValue); setSelectedErrorTypes([]); }; @@ -239,7 +189,6 @@ export const GlobalErrorsFilters = () => { trackingEvents.GLOBAL_ERRORS_VIEW_ERROR_TYPES_FILTER_CHANGED ); const newValue = Array.isArray(value) ? value : [value]; - setLastChangedFilter("Endpoints"); setSelectedErrorTypes(newValue); }; @@ -320,18 +269,18 @@ export const GlobalErrorsFilters = () => { () => [ { label: "Yes", - value: "Handled", + value: "Unhandled", selected: selectedHandlingTypes.length === 1 && - selectedHandlingTypes[0] === "Handled", + selectedHandlingTypes[0] === "Unhandled", enabled: true }, { label: "No", - value: "Unhandled", + value: "Handled", selected: selectedHandlingTypes.length === 1 && - selectedHandlingTypes[0] === "Unhandled", + selectedHandlingTypes[0] === "Handled", enabled: true }, { @@ -364,7 +313,9 @@ export const GlobalErrorsFilters = () => { )} - disabled={servicesFilterOptions?.length === 0} + disabled={ + servicesFilterOptions?.length === 0 || !areFilterOptionsLoaded + } /> ) }, @@ -379,13 +330,16 @@ export const GlobalErrorsFilters = () => { endpointsFilterOptions, "Endpoints" )} + searchable={true} multiselect={true} icon={(props: IconProps) => ( )} - disabled={endpointsFilterOptions?.length === 0} + disabled={ + endpointsFilterOptions?.length === 0 || !areFilterOptionsLoaded + } /> ) }, @@ -406,7 +360,9 @@ export const GlobalErrorsFilters = () => { )} - disabled={errorTypesFilterOptions?.length === 0} + disabled={ + errorTypesFilterOptions?.length === 0 || !areFilterOptionsLoaded + } /> ) }, @@ -429,6 +385,7 @@ export const GlobalErrorsFilters = () => { )} + disabled={!areFilterOptionsLoaded} /> ) }, @@ -445,6 +402,7 @@ export const GlobalErrorsFilters = () => { )} + disabled={!areFilterOptionsLoaded} /> ) } @@ -483,7 +441,6 @@ export const GlobalErrorsFilters = () => { setGlobalErrorsSelectedFilters({ ...globalErrorsSelectedFilters, - services: selectedServices, endpoints: selectedEndpoints, errorTypes: selectedErrorTypes, criticalities: selectedCriticalities, @@ -513,10 +470,10 @@ export const GlobalErrorsFilters = () => { const appliedFiltersCount = [ (globallySelectedServices ?? []).length, - globalErrorsSelectedFilters.endpoints.length, - globalErrorsSelectedFilters.errorTypes.length, - globalErrorsSelectedFilters.criticalities.length, - globalErrorsSelectedFilters.handlingTypes.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/GlobalErrorsList/index.tsx b/src/components/Errors/GlobalErrorsList/index.tsx index 49d8bde7b..9e05dd6ac 100644 --- a/src/components/Errors/GlobalErrorsList/index.tsx +++ b/src/components/Errors/GlobalErrorsList/index.tsx @@ -53,7 +53,7 @@ export const GlobalErrorsList = () => { const listContainerRef = useRef(null); const [latestPinChangedId, setLatestPinChangedId] = useState(); const [areAnimationsEnabled, setAreAnimationsEnabled] = useState(false); - const { environment, backendInfo } = useConfigSelector(); + const { environment, backendInfo, selectedServices } = useConfigSelector(); const isDismissEnabled = getFeatureFlagValue( backendInfo, FeatureFlag.IS_GLOBAL_ERROR_DISMISS_ENABLED @@ -81,7 +81,8 @@ export const GlobalErrorsList = () => { resetGlobalErrors, resetGlobalErrorsSelectedFilters, setGlobalErrorsViewMode, - setGlobalErrorsLastDays + setGlobalErrorsLastDays, + setSelectedServices } = useStore.getState(); const areGlobalErrorsFiltersEnabled = getFeatureFlagValue( @@ -101,6 +102,7 @@ export const GlobalErrorsList = () => { ); const environmentId = environment?.id; + const previousEnvironmentId = usePrevious(environmentId); const sortingMenuItems = Object.values(GLOBAL_ERROR_SORTING_CRITERION).map( (x) => ({ @@ -132,15 +134,15 @@ export const GlobalErrorsList = () => { dismissed: mode === ViewMode.OnlyDismissed, ...(areGlobalErrorsFiltersEnabled ? { - services: selectedFilters.services, - endpoints: selectedFilters.endpoints, - errorTypes: selectedFilters.errorTypes + services: selectedServices ?? [], + endpoints: selectedFilters?.endpoints ?? [], + errorTypes: selectedFilters?.errorTypes ?? [] } : {}), ...(areGlobalErrorsCriticalityAndUnhandledFiltersEnabled ? { - criticalities: selectedFilters.criticalities, - handlingTypes: selectedFilters.handlingTypes + criticalities: selectedFilters?.criticalities ?? [], + handlingTypes: selectedFilters?.handlingTypes ?? [] } : {}) }), @@ -152,11 +154,11 @@ export const GlobalErrorsList = () => { mode, lastDays, areGlobalErrorsFiltersEnabled, - selectedFilters.services, - selectedFilters.endpoints, - selectedFilters.errorTypes, - selectedFilters.criticalities, - selectedFilters.handlingTypes, + selectedServices, + selectedFilters?.endpoints, + selectedFilters?.errorTypes, + selectedFilters?.criticalities, + selectedFilters?.handlingTypes, areGlobalErrorsCriticalityAndUnhandledFiltersEnabled ] ); @@ -232,17 +234,19 @@ export const GlobalErrorsList = () => { environmentId, search, setGlobalErrorsPage, - selectedFilters.services, - selectedFilters.endpoints, - selectedFilters.errorTypes, - selectedFilters.criticalities, - selectedFilters.handlingTypes + selectedServices, + selectedFilters?.endpoints, + selectedFilters?.errorTypes, + selectedFilters?.criticalities, + selectedFilters?.handlingTypes ]); // Reset filters on environment change useEffect(() => { - resetGlobalErrorsSelectedFilters(); - }, [environmentId, resetGlobalErrorsSelectedFilters]); + if (previousEnvironmentId && environmentId !== previousEnvironmentId) { + resetGlobalErrorsSelectedFilters(); + } + }, [previousEnvironmentId, environmentId, resetGlobalErrorsSelectedFilters]); // Reset scroll position on filters change useEffect(() => { @@ -310,6 +314,7 @@ export const GlobalErrorsList = () => { setGlobalErrorsSearch(""); setGlobalErrorsPage(0); resetGlobalErrorsSelectedFilters(); + setSelectedServices([]); }; const handleDismissalViewModeButtonClick = () => { @@ -338,11 +343,11 @@ export const GlobalErrorsList = () => { const areAnyFiltersApplied = search || [ - selectedFilters.services, - selectedFilters.endpoints, - selectedFilters.errorTypes, - selectedFilters.criticalities, - selectedFilters.handlingTypes + selectedServices ?? [], + selectedFilters?.endpoints ?? [], + selectedFilters?.errorTypes ?? [], + selectedFilters?.criticalities ?? [], + selectedFilters?.handlingTypes ?? [] ].some((x) => x.length > 0); const renderDismissBtn = () => ( diff --git a/src/components/Errors/index.tsx b/src/components/Errors/index.tsx index be0f16b70..8f1807489 100644 --- a/src/components/Errors/index.tsx +++ b/src/components/Errors/index.tsx @@ -4,6 +4,7 @@ import { getFeatureFlagValue } from "../../featureFlags"; import { usePersistence } from "../../hooks/usePersistence"; import { usePrevious } from "../../hooks/usePrevious"; import { useConfigSelector } from "../../store/config/useConfigSelector"; +import { GlobalErrorsSelectedFiltersState } from "../../store/errors/errorsSlice"; import { useErrorsSelector } from "../../store/errors/useErrorsSelector"; import { useStore } from "../../store/useStore"; import { trackingEvents as globalEvents } from "../../trackingEvents"; @@ -21,10 +22,17 @@ import { ErrorsList } from "./ErrorsList"; import { GlobalErrorsList } from "./GlobalErrorsList"; import * as s from "./styles"; +const PERSISTENCE_KEY = "globalErrorsFilters"; const SHOW_ONLY_WORKSPACE_ERROR_STACK_TRACE_ITEMS_PERSISTENCE_KEY = "showOnlyWorkspaceErrorStackTraceItems"; export const Errors = () => { + const [persistedFilters, setPersistedFilters] = + usePersistence( + PERSISTENCE_KEY, + "project" + ); + const previousPersistedFilters = usePrevious(persistedFilters); const [persistedShowWorkspaceItemsOnly, setPersistedShowWorkspaceItemsOnly] = usePersistence( SHOW_ONLY_WORKSPACE_ERROR_STACK_TRACE_ITEMS_PERSISTENCE_KEY, @@ -38,12 +46,14 @@ export const Errors = () => { const previousPersistedShowWorkspaceItemsOnly = usePrevious( persistedShowWorkspaceItemsOnly ); - const { scope, backendInfo } = useConfigSelector(); - const { errorDetailsWorkspaceItemsOnly } = useErrorsSelector(); + const { scope, backendInfo, selectedServices } = useConfigSelector(); + const { errorDetailsWorkspaceItemsOnly, globalErrorsSelectedFilters } = + useErrorsSelector(); const previousErrorDetailsWorkspaceItemsOnly = usePrevious( errorDetailsWorkspaceItemsOnly ); - const { setErrorDetailsWorkspaceItemsOnly } = useStore.getState(); + const { setErrorDetailsWorkspaceItemsOnly, setGlobalErrorsSelectedFilters } = + useStore.getState(); const spanCodeObjectId = scope?.span?.spanCodeObjectId; const methodId = scope?.span?.methodId ?? undefined; const { goTo } = useHistory(); @@ -53,6 +63,35 @@ export const Errors = () => { backendInfo, FeatureFlag.ARE_GLOBAL_ERRORS_ENABLED ); + const isInitialized = Boolean(globalErrorsSelectedFilters); + + useEffect(() => { + if ( + isUndefined(previousPersistedFilters) && + !isUndefined(persistedFilters) + ) { + setGlobalErrorsSelectedFilters( + persistedFilters ?? { + endpoints: [], + errorTypes: [], + criticalities: [], + handlingTypes: [] + } + ); + } + }, [ + previousPersistedFilters, + persistedFilters, + selectedServices, + setGlobalErrorsSelectedFilters + ]); + + // Update persisted filters on filters change + useEffect(() => { + if (isInitialized) { + setPersistedFilters(globalErrorsSelectedFilters); + } + }, [isInitialized, globalErrorsSelectedFilters, setPersistedFilters]); // Rehydrate "Workspace only" toggle value from persistence useEffect(() => { @@ -104,6 +143,10 @@ export const Errors = () => { goTo(`/${TAB_IDS.ASSETS}`); }; + if (!isInitialized) { + return null; + } + const renderContent = () => { if (selectedErrorId) { return ( diff --git a/src/components/IdeLauncher/index.tsx b/src/components/IdeLauncher/index.tsx index 5ad406f18..e0ddc270a 100644 --- a/src/components/IdeLauncher/index.tsx +++ b/src/components/IdeLauncher/index.tsx @@ -271,6 +271,7 @@ export const IdeLauncher = () => { Digma IDE Plugin Launcher + { + const [minWidth, setMinWidth] = useState(); + const [maxWidth, setMaxWidth] = useState(); + const arrowRef = useRef(null); const theme = useTheme(); const arrowColor = getArrowColor(theme); @@ -87,10 +92,43 @@ export const NewPopover = ({ onOpenChange: onOpenChange, middleware: [ offset(showArrow ? ARROW_HEIGHT + ARROW_GAP : GAP), - flip(), - shift({ - boundary + size({ + apply({ rects, availableWidth }) { + if (sameWidth) { + setMinWidth(rects.reference.width); + setMaxWidth(rects.reference.width); + } else { + const safeAvailableWidth = Math.max(availableWidth, 0); + const viewportWidth = window.innerWidth; + const anchorLeft = rects.reference.x; + + const minWidth = width; + const maxWidth = + useShift === false + ? Math.max(viewportWidth - anchorLeft, 0) + : width; + + setMinWidth( + isNumber(minWidth) + ? Math.min(minWidth, safeAvailableWidth) + : minWidth + ); + setMaxWidth( + isNumber(maxWidth) + ? Math.min(maxWidth, safeAvailableWidth) + : maxWidth + ); + } + } }), + flip(), + ...(useShift === false + ? [] + : [ + shift({ + boundary + }) + ]), ...(showArrow ? [ arrow({ @@ -126,11 +164,8 @@ export const NewPopover = ({ ref={refs.setFloating} style={{ ...floatingStyles, - width: sameWidth - ? context.elements.reference?.getBoundingClientRect().width - : isUndefined(width) - ? undefined - : width, + minWidth, + maxWidth, zIndex: LAYERS.MODAL }} {...getFloatingProps()} diff --git a/src/components/common/NewPopover/types.ts b/src/components/common/NewPopover/types.ts index d6416042c..99ba529cf 100644 --- a/src/components/common/NewPopover/types.ts +++ b/src/components/common/NewPopover/types.ts @@ -13,4 +13,5 @@ export interface PopoverProps { sameWidth?: boolean; useClickInteraction?: boolean; closeOnOutsidePress?: boolean; + useShift?: boolean; } diff --git a/src/components/common/v3/Select/index.tsx b/src/components/common/v3/Select/index.tsx index 8f35d6705..1606c4503 100644 --- a/src/components/common/v3/Select/index.tsx +++ b/src/components/common/v3/Select/index.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useCallback, useRef, useState } from "react"; +import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react"; import useDimensions from "react-cool-dimensions"; import { isString } from "../../../../typeGuards/isString"; import { isUndefined } from "../../../../typeGuards/isUndefined"; @@ -32,7 +32,9 @@ export const Select = ({ placeholder, className, searchable, - showSelectedState + showSelectedState, + useShift, + sameWidth }: SelectProps) => { const [isOpen, setIsOpen] = useState(false); const [searchValue, setSearchValue] = useState(""); @@ -49,7 +51,6 @@ export const Select = ({ const handleButtonClick = () => { setIsOpen(!isOpen); - setSearchValue(""); }; const handleItemClick = (item: SelectItem) => { @@ -60,7 +61,6 @@ export const Select = ({ if (!multiselect) { onChange(item.value); setIsOpen(false); - setSearchValue(""); return; } @@ -79,6 +79,12 @@ export const Select = ({ setSearchValue(e.target.value); }; + useEffect(() => { + if (!isOpen) { + setSearchValue(""); + } + }, [isOpen]); + const selectedValues = items.filter((x) => x.selected).map((x) => x.value); const filteredItems = items.filter((x) => @@ -102,7 +108,8 @@ export const Select = ({ isOpen || (isSelectedStateEnabled && selectedValues.length > 0); return ( {isSearchBarVisible && ( diff --git a/src/components/common/v3/Select/types.ts b/src/components/common/v3/Select/types.ts index ca072eacd..72c54cbd6 100644 --- a/src/components/common/v3/Select/types.ts +++ b/src/components/common/v3/Select/types.ts @@ -27,6 +27,8 @@ export interface SelectProps { icon?: ComponentType; className?: string; showSelectedState?: boolean; + useShift?: boolean; + sameWidth?: boolean; } export interface ButtonProps { diff --git a/src/hooks/useFetchData.ts b/src/hooks/useFetchData.ts index 96648f969..94c4137f0 100644 --- a/src/hooks/useFetchData.ts +++ b/src/hooks/useFetchData.ts @@ -41,7 +41,7 @@ export const useFetchData = ( refreshWithDebounce = false, debounceDelay = 0 }: DataFetcherConfiguration, - query?: T + payload?: T ) => { const [data, setData] = useState(); const [lastSetDataTimeStamp, setLastSetDataTimeStamp] = useState(); @@ -53,24 +53,17 @@ export const useFetchData = ( () => refreshWithInterval && refreshInterval > 0, [refreshWithInterval, refreshInterval] ); - const isDebounceEnabled = useMemo( - () => refreshWithDebounce && debounceDelay >= 0, - [refreshWithDebounce, debounceDelay] - ); const isPreviousRefreshWithIntervalEnabled = usePrevious( isRefreshWithIntervalEnabled ); const previousRefreshInterval = usePrevious(refreshInterval); const previousIsEnabled = usePrevious(isEnabled); - const [payload, setPayload] = useState( - !isDebounceEnabled ? query : undefined - ); const previousPayload = usePrevious(payload); const [isMounted, setIsMounted] = useState(false); useMount(() => { if (isEnabled && fetchOnMount) { - sendMessage(requestAction, query); + sendMessage(requestAction, payload); } setIsMounted(true); @@ -80,22 +73,6 @@ export const useFetchData = ( }; }); - useEffect(() => { - if (!isMounted) { - return; - } - - if (!isDebounceEnabled) { - setPayload(query); - return; - } - - window.clearTimeout(debounceTimerId.current); - debounceTimerId.current = window.setTimeout(() => { - setPayload(query); - }, debounceDelay); - }, [debounceDelay, query, isMounted, isDebounceEnabled]); - // Clear timer and get data on request action change useEffect(() => { if (isEnabled && isMounted && previousRequestAction !== requestAction) { @@ -121,7 +98,14 @@ export const useFetchData = ( previousPayload !== payload ) { window.clearTimeout(refreshTimerId.current); - sendMessage(requestAction, payload); + + if (refreshWithDebounce && debounceDelay >= 0) { + refreshTimerId.current = window.setTimeout(() => { + sendMessage(requestAction, payload); + }, debounceDelay); + } else { + sendMessage(requestAction, payload); + } } }, [ previousPayload, @@ -152,6 +136,7 @@ export const useFetchData = ( isPreviousRefreshWithIntervalEnabled !== isRefreshWithIntervalEnabled ) { window.clearTimeout(refreshTimerId.current); + if (isEnabled && isRefreshWithIntervalEnabled) { sendMessage(requestAction, payload); } diff --git a/src/store/assets/assetsSlice.ts b/src/store/assets/assetsSlice.ts index 5c288f7e4..310bee8e2 100644 --- a/src/store/assets/assetsSlice.ts +++ b/src/store/assets/assetsSlice.ts @@ -78,7 +78,7 @@ export const assetsSlice = createSlice({ setAssets: (assets: AssetsData) => set({ assets }), setAreAssetsLoading: (isLoading: boolean) => set({ areAssetsLoading: isLoading }), - setAssetsFilters: (filters: AssetsFilters) => set({ filters }), + setAssetsFilters: (filters: AssetsFilters | null) => set({ filters }), setAssetsViewMode: (viewMode: ViewMode) => set({ viewMode }), setAssetsSearch: (search: string) => set({ search }), setAssetsPage: (page: number) => set({ page }), diff --git a/src/store/errors/errorsSlice.ts b/src/store/errors/errorsSlice.ts index 4120eaf83..a2fbadd6e 100644 --- a/src/store/errors/errorsSlice.ts +++ b/src/store/errors/errorsSlice.ts @@ -29,7 +29,6 @@ export interface GlobalErrorsFiltersState { } export interface GlobalErrorsSelectedFiltersState { - services: string[]; endpoints: string[]; errorTypes: string[]; criticalities: ErrorCriticality[]; @@ -45,21 +44,20 @@ export interface ErrorsState { globalErrorsPageSize: number; globalErrorsSorting: GLOBAL_ERROR_SORTING_CRITERION; globalErrorsFilters: GlobalErrorsFiltersState; - globalErrorsSelectedFilters: GlobalErrorsSelectedFiltersState; + globalErrorsSelectedFilters: GlobalErrorsSelectedFiltersState | null; globalErrorsViewMode: ViewMode; errorDetailsWorkspaceItemsOnly: boolean; globalErrorsLastDays?: number; } -const selectedFiltersInitialState = { - services: [], +const selectedFiltersInitialState: GlobalErrorsSelectedFiltersState = { endpoints: [], errorTypes: [], criticalities: [], handlingTypes: [] }; -export const globalErrorsInitialState = { +export const globalErrorsWithoutFiltersInitialState = { globalErrorsList: null, globalErrorsTotalCount: 0, areGlobalErrorsLoading: false, @@ -73,10 +71,15 @@ export const globalErrorsInitialState = { errorTypes: null }, globalErrorsViewMode: ViewMode.All, - globalErrorsSelectedFilters: selectedFiltersInitialState, + globalErrorsSelectedFilters: null, globalErrorsLastDays: DAYS_FILTER_DEFAULT_VALUE }; +export const globalErrorsInitialState = { + ...globalErrorsWithoutFiltersInitialState, + globalErrorsSelectedFilters: selectedFiltersInitialState +}; + export const initialState: ErrorsState = { ...globalErrorsInitialState, errorDetailsWorkspaceItemsOnly: false @@ -119,6 +122,6 @@ export const errorsSlice = createSlice({ set({ globalErrorsViewMode: mode }), setGlobalErrorsLastDays: (days?: number) => set({ globalErrorsLastDays: days }), - resetGlobalErrors: () => set({ ...globalErrorsInitialState }) + resetGlobalErrors: () => set({ ...globalErrorsWithoutFiltersInitialState }) } }); diff --git a/src/utils/fetchData.ts b/src/utils/fetchData.ts new file mode 100644 index 000000000..cb860344a --- /dev/null +++ b/src/utils/fetchData.ts @@ -0,0 +1,23 @@ +import { dispatcher } from "../dispatcher"; + +export const fetchData = async ( + config: { + requestAction: string; + responseAction: string; + }, + payload: T +) => { + return new Promise((resolve) => { + window.sendMessageToDigma({ + action: config.requestAction, + payload + }); + + const handleResponse = (data: unknown) => { + resolve(data as K); + dispatcher.removeActionListener(config.responseAction, handleResponse); + }; + + dispatcher.addActionListener(config.responseAction, handleResponse); + }); +};