diff --git a/packages/api-v4/.changeset/pr-10853-upcoming-features-1725371578425.md b/packages/api-v4/.changeset/pr-10853-upcoming-features-1725371578425.md new file mode 100644 index 00000000000..96feffe8820 --- /dev/null +++ b/packages/api-v4/.changeset/pr-10853-upcoming-features-1725371578425.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Add export to FilterValue interface ([#10853](https://github.com/linode/manager/pull/10853)) diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index 2c9c7f57662..9d24aca5857 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -11,6 +11,7 @@ export interface Dashboard { export interface TimeGranularity { unit: string; value: number; + label?: string; } export interface TimeDuration { @@ -44,8 +45,7 @@ export interface Filters { value: string; } -// Define the type for filter values -type FilterValue = +export type FilterValue = | number | string | string[] @@ -56,7 +56,6 @@ type FilterValue = type WidgetFilterValue = { [key: string]: AclpWidget }; export interface AclpConfig { - // we maintain only the filters selected in the preferences for latest selected dashboard [key: string]: FilterValue; widgets?: WidgetFilterValue; } diff --git a/packages/manager/.changeset/pr-10853-upcoming-features-1725371660855.md b/packages/manager/.changeset/pr-10853-upcoming-features-1725371660855.md new file mode 100644 index 00000000000..90e7665b557 --- /dev/null +++ b/packages/manager/.changeset/pr-10853-upcoming-features-1725371660855.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +add useAclpPreference hook in UserPreference.ts, update CloudPulseWidget.ts, CloudPulseDashboard & GlobalFilters to use useAclpPreference and pass preference values to child component ([#10853](https://github.com/linode/manager/pull/10853)) diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx index 837f5e54c21..220c8b2813b 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx @@ -1,10 +1,8 @@ -import { Grid, Paper } from '@mui/material'; +import { Grid } from '@mui/material'; import React from 'react'; -import CloudPulseIcon from 'src/assets/icons/entityIcons/monitor.svg'; import { CircleProgress } from 'src/components/CircleProgress'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; -import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { useCloudPulseDashboardByIdQuery } from 'src/queries/cloudpulse/dashboards'; import { useResourcesQuery } from 'src/queries/cloudpulse/resources'; import { @@ -12,26 +10,11 @@ import { useGetCloudPulseMetricDefinitionsByServiceType, } from 'src/queries/cloudpulse/services'; -import { getUserPreferenceObject } from '../Utils/UserPreference'; -import { createObjectCopy } from '../Utils/utils'; -import { CloudPulseWidget } from '../Widget/CloudPulseWidget'; -import { - allIntervalOptions, - getInSeconds, - getIntervalIndex, -} from '../Widget/components/CloudPulseIntervalSelect'; +import { useAclpPreference } from '../Utils/UserPreference'; +import { RenderWidgets } from '../Widget/CloudPulseWidgetRenderer'; -import type { - CloudPulseMetricsAdditionalFilters, - CloudPulseWidgetProperties, -} from '../Widget/CloudPulseWidget'; -import type { - AvailableMetrics, - Dashboard, - JWETokenPayLoad, - TimeDuration, - Widgets, -} from '@linode/api-v4'; +import type { CloudPulseMetricsAdditionalFilters } from '../Widget/CloudPulseWidget'; +import type { JWETokenPayLoad, TimeDuration } from '@linode/api-v4'; export interface DashboardProperties { /** @@ -52,7 +35,7 @@ export interface DashboardProperties { /** * optional timestamp to pass as react query param to forcefully re-fetch data */ - manualRefreshTimeStamp?: number | undefined; + manualRefreshTimeStamp?: number; /** * Selected region for the dashboard @@ -80,53 +63,14 @@ export const CloudPulseDashboard = (props: DashboardProperties) => { savePref, } = props; + const { preferences } = useAclpPreference(); + const getJweTokenPayload = (): JWETokenPayLoad => { return { resource_ids: resourceList?.map((resource) => Number(resource.id)) ?? [], }; }; - const getCloudPulseGraphProperties = ( - widget: Widgets - ): CloudPulseWidgetProperties => { - const graphProp: CloudPulseWidgetProperties = { - additionalFilters, - ariaLabel: widget.label, - authToken: '', - availableMetrics: undefined, - duration, - errorLabel: 'Error While Loading Data', - resourceIds: resources, - resources: [], - serviceType: dashboard?.service_type ?? '', - timeStamp: manualRefreshTimeStamp, - unit: widget.unit ?? '%', - widget: { ...widget }, - }; - if (savePref) { - setPreferredWidgetPlan(graphProp.widget); - } - return graphProp; - }; - - const setPreferredWidgetPlan = (widgetObj: Widgets) => { - const widgetPreferences = getUserPreferenceObject().widgets; - const pref = widgetPreferences?.[widgetObj.label]; - if (pref) { - Object.assign(widgetObj, { - aggregate_function: pref.aggregateFunction, - size: pref.size, - time_granularity: { ...pref.timeGranularity }, - }); - } - }; - - const getTimeGranularity = (scrapeInterval: string) => { - const scrapeIntervalValue = getInSeconds(scrapeInterval); - const index = getIntervalIndex(scrapeIntervalValue); - return index < 0 ? allIntervalOptions[0] : allIntervalOptions[index]; - }; - const { data: dashboard, isLoading: isDashboardLoading, @@ -182,76 +126,18 @@ export const CloudPulseDashboard = (props: DashboardProperties) => { return ; } - const RenderWidgets = () => { - if (!dashboard || !dashboard.widgets?.length) { - return renderPlaceHolder( - 'No visualizations are available at this moment. Create Dashboards to list here.' - ); - } - - if ( - !dashboard.service_type || - !Boolean(resources.length > 0) || - !jweToken?.token || - !Boolean(resourceList?.length) - ) { - return renderPlaceHolder( - 'Select Dashboard, Region and Resource to visualize metrics' - ); - } - - // maintain a copy - const newDashboard: Dashboard = createObjectCopy(dashboard)!; - return ( - - {{ ...newDashboard }.widgets.map((widget, index) => { - // check if widget metric definition is available or not - if (widget) { - // find the metric defintion of the widget label - const availMetrics = metricDefinitions?.data.find( - (availMetrics: AvailableMetrics) => - widget.label === availMetrics.label - ); - const cloudPulseWidgetProperties = getCloudPulseGraphProperties({ - ...widget, - }); - - // metric definition is available but time_granularity is not present - if ( - availMetrics && - !cloudPulseWidgetProperties.widget.time_granularity - ) { - cloudPulseWidgetProperties.widget.time_granularity = getTimeGranularity( - availMetrics.scrape_interval - ); - } - return ( - - ); - } else { - return ; - } - })} - - ); - }; - - const renderPlaceHolder = (title: string) => { - return ( - - - - - - ); - }; - - return ; + return ( + + ); }; diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx index 3907346f617..828a05301c7 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding.tsx @@ -1,39 +1,35 @@ import { Grid, Paper } from '@mui/material'; import * as React from 'react'; -import CloudPulseIcon from 'src/assets/icons/entityIcons/monitor.svg'; -import { CircleProgress } from 'src/components/CircleProgress'; -import { StyledPlaceholder } from 'src/features/StackScripts/StackScriptBase/StackScriptBase.styles'; - import { GlobalFilters } from '../Overview/GlobalFilters'; -import { REFRESH, REGION, RESOURCE_ID } from '../Utils/constants'; -import { - checkIfAllMandatoryFiltersAreSelected, - getMetricsCallCustomFilters, -} from '../Utils/FilterBuilder'; -import { FILTER_CONFIG } from '../Utils/FilterConfig'; -import { useLoadUserPreferences } from '../Utils/UserPreference'; -import { CloudPulseDashboard } from './CloudPulseDashboard'; +import { CloudPulseDashboardRenderer } from './CloudPulseDashboardRenderer'; import type { Dashboard, TimeDuration } from '@linode/api-v4'; export type FilterValueType = number | number[] | string | string[] | undefined; +export interface DashboardProp { + dashboard?: Dashboard; + filterValue: { + [key: string]: FilterValueType; + }; + timeDuration?: TimeDuration; +} + export const CloudPulseDashboardLanding = () => { const [filterValue, setFilterValue] = React.useState<{ [key: string]: FilterValueType; }>({}); - const [timeDuration, setTimeDuration] = React.useState(); const [dashboard, setDashboard] = React.useState(); - const selectDashboardAndFilterMessage = - 'Select Dashboard and filters to visualize metrics.'; - const onFilterChange = React.useCallback( (filterKey: string, filterValue: FilterValueType) => { - setFilterValue((prev) => ({ ...prev, [filterKey]: filterValue })); + setFilterValue((prev: { [key: string]: FilterValueType }) => ({ + ...prev, + [filterKey]: filterValue, + })); }, [] ); @@ -42,96 +38,14 @@ export const CloudPulseDashboardLanding = () => { setDashboard(dashboardObj); setFilterValue({}); // clear the filter values on dashboard change }, []); - const onTimeDurationChange = React.useCallback( (timeDurationObj: TimeDuration) => { setTimeDuration(timeDurationObj); }, [] ); - - const { isLoading } = useLoadUserPreferences(); - - /** - * Takes an error message as input and renders a placeholder with the error message - * @param errorMessage {string} - Error message which will be displayed - * - */ - const renderErrorPlaceholder = (errorMessage: string) => { - return ( - - - - - - ); - }; - - /** - * Incase of errors and filter criteria not met, this renders the required error message placeholder and in case of success checks, renders a dashboard - * @returns Placeholder | Dashboard - */ - const RenderDashboard = () => { - if (!dashboard) { - return renderErrorPlaceholder(selectDashboardAndFilterMessage); - } - - if (!FILTER_CONFIG.get(dashboard.service_type)) { - return renderErrorPlaceholder( - "No Filters Configured for selected dashboard's service type" - ); - } - - if ( - !checkIfAllMandatoryFiltersAreSelected({ - dashboard, - filterValue, - timeDuration, - }) || - !timeDuration - ) { - return renderErrorPlaceholder(selectDashboardAndFilterMessage); - } - - return ( - - ); - }; - - if (isLoading) { - return ; - } - return ( - + { /> - + ); }; diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardRenderer.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardRenderer.tsx new file mode 100644 index 00000000000..83abd618a98 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardRenderer.tsx @@ -0,0 +1,79 @@ +import React from 'react'; + +import { CloudPulseErrorPlaceholder } from '../shared/CloudPulseErrorPlaceholder'; +import { REFRESH, REGION, RESOURCE_ID } from '../Utils/constants'; +import { + checkIfAllMandatoryFiltersAreSelected, + getMetricsCallCustomFilters, +} from '../Utils/FilterBuilder'; +import { FILTER_CONFIG } from '../Utils/FilterConfig'; +import { CloudPulseDashboard } from './CloudPulseDashboard'; + +import type { DashboardProp } from './CloudPulseDashboardLanding'; + +export const CloudPulseDashboardRenderer = React.memo( + (props: DashboardProp) => { + const { dashboard, filterValue, timeDuration } = props; + + const selectDashboardAndFilterMessage = + 'Select Dashboard and filters to visualize metrics.'; + + const getMetricsCall = React.useMemo( + () => getMetricsCallCustomFilters(filterValue, dashboard?.service_type), + [dashboard?.service_type, filterValue] + ); + + if (!dashboard) { + return ( + + ); + } + + if (!FILTER_CONFIG.get(dashboard.service_type)) { + return ( + + ); + } + + if ( + !checkIfAllMandatoryFiltersAreSelected({ + dashboard, + filterValue, + timeDuration, + }) || + !timeDuration + ) { + return ( + + ); + } + + return ( + + ); + } +); diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx index 0b3d44ae1ac..68bf5750647 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx @@ -10,7 +10,6 @@ const queryMocks = vi.hoisted(() => ({ useCloudPulseDashboardByIdQuery: vi.fn().mockReturnValue({}), })); -const selectTimeDurationPlaceholder = 'Select a Time Duration'; const circleProgress = 'circle-progress'; const mandatoryFiltersError = 'Mandatory Filters not Selected'; @@ -63,9 +62,6 @@ describe('CloudPulseDashboardWithFilters component tests', () => { ); - expect( - screen.getByPlaceholderText(selectTimeDurationPlaceholder) - ).toBeDefined(); expect(screen.getByTestId(circleProgress)).toBeDefined(); // the dashboards started to render }); @@ -81,9 +77,6 @@ describe('CloudPulseDashboardWithFilters component tests', () => { ); - expect( - screen.getByPlaceholderText(selectTimeDurationPlaceholder) - ).toBeDefined(); expect(screen.getByTestId(circleProgress)).toBeDefined(); // the dashboards started to render }); diff --git a/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.test.tsx b/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.test.tsx new file mode 100644 index 00000000000..d2d9e9a2ebc --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.test.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { GlobalFilters } from './GlobalFilters'; + +const mockHandleAnyFilterChange = vi.fn(); +const mockHandleDashboardChange = vi.fn(); +const mockHandleTimeDurationChange = vi.fn(); +const timeRangeSelectId = 'cloudpulse-time-duration'; +const setup = () => { + return renderWithTheme( + + ); +}; +describe('Global filters component test', () => { + it('Should render refresh button', () => { + const { getByTestId } = setup(); + expect(getByTestId('global-refresh')).toBeInTheDocument(); + }), + it('Should show dashboard selectcomponent', () => { + const { getByTestId } = setup(); + + expect(getByTestId('cloudpulse-dashboard-select')).toBeInTheDocument(); + }), + it('Should have time range select with default value', () => { + const screen = setup(); + + const timeRangeSelect = screen.getByTestId(timeRangeSelectId); + + expect(timeRangeSelect).toBeInTheDocument(); + + expect( + screen.getByRole('combobox', { name: 'Select Time Duration' }) + ).toHaveAttribute('value', 'Last 30 Minutes'); + }); +}); diff --git a/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx b/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx index b6b290176d7..0cd8b7c95bf 100644 --- a/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx +++ b/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx @@ -9,11 +9,11 @@ import { Divider } from 'src/components/Divider'; import { CloudPulseDashboardFilterBuilder } from '../shared/CloudPulseDashboardFilterBuilder'; import { CloudPulseDashboardSelect } from '../shared/CloudPulseDashboardSelect'; import { CloudPulseTimeRangeSelect } from '../shared/CloudPulseTimeRangeSelect'; -import { REFRESH } from '../Utils/constants'; +import { DASHBOARD_ID, REFRESH, TIME_DURATION } from '../Utils/constants'; +import { useAclpPreference } from '../Utils/UserPreference'; import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; -import type { Dashboard, TimeDuration } from '@linode/api-v4'; -import type { WithStartAndEnd } from 'src/features/Longview/request.types'; +import type { AclpConfig, Dashboard, TimeDuration } from '@linode/api-v4'; export interface GlobalFilterProperties { handleAnyFilterChange(filterKey: string, filterValue: FilterValueType): void; @@ -21,55 +21,66 @@ export interface GlobalFilterProperties { handleTimeDurationChange(timeDuration: TimeDuration): void; } -export interface FiltersObject { - interval: string; - region: string; - resource: string[]; - serviceType?: string; - timeRange: WithStartAndEnd; -} - export const GlobalFilters = React.memo((props: GlobalFilterProperties) => { const { handleAnyFilterChange, handleDashboardChange, handleTimeDurationChange, } = props; + + const { + preferences, + updateGlobalFilterPreference: updatePreferences, + } = useAclpPreference(); const [selectedDashboard, setSelectedDashboard] = React.useState< Dashboard | undefined >(); const handleTimeRangeChange = React.useCallback( - (timerDuration: TimeDuration) => { + ( + timerDuration: TimeDuration, + timeDurationValue: string = 'Auto', + savePref: boolean = false + ) => { + if (savePref) { + updatePreferences({ [TIME_DURATION]: timeDurationValue }); + } handleTimeDurationChange(timerDuration); }, - [handleTimeDurationChange] + [] ); const onDashboardChange = React.useCallback( - (dashboard: Dashboard | undefined) => { + (dashboard: Dashboard | undefined, savePref: boolean = false) => { + if (savePref) { + updatePreferences({ + [DASHBOARD_ID]: dashboard?.id, + }); + } setSelectedDashboard(dashboard); handleDashboardChange(dashboard); }, - [handleDashboardChange] + [] ); const emitFilterChange = React.useCallback( - (filterKey: string, value: FilterValueType) => { + ( + filterKey: string, + value: FilterValueType, + savePref: boolean = false, + updatedPreferenceData: AclpConfig = {} + ) => { + if (savePref) { + updatePreferences(updatedPreferenceData); + } handleAnyFilterChange(filterKey, value); }, - [handleAnyFilterChange] + [] ); - const handleGlobalRefresh = React.useCallback( - (dashboardObj?: Dashboard) => { - if (!dashboardObj) { - return; - } - handleAnyFilterChange(REFRESH, Date.now()); - }, - [handleAnyFilterChange] - ); + const handleGlobalRefresh = React.useCallback(() => { + handleAnyFilterChange(REFRESH, Date.now()); + }, []); const theme = useTheme(); @@ -85,21 +96,26 @@ export const GlobalFilters = React.memo((props: GlobalFilterProperties) => { > handleGlobalRefresh(selectedDashboard)} + onClick={handleGlobalRefresh} size="small" > @@ -123,6 +139,7 @@ export const GlobalFilters = React.memo((props: GlobalFilterProperties) => { dashboard={selectedDashboard} emitFilterChange={emitFilterChange} isServiceAnalyticsIntegration={false} + preferences={preferences} /> )} diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts index 57a6b6e7bbc..2a0ee2cd64a 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts @@ -1,4 +1,9 @@ -import { RELATIVE_TIME_DURATION, RESOURCE_ID, RESOURCES } from './constants'; +import { + REGION, + RELATIVE_TIME_DURATION, + RESOURCE_ID, + RESOURCES, +} from './constants'; import { FILTER_CONFIG } from './FilterConfig'; import { CloudPulseSelectTypes } from './models'; @@ -12,7 +17,13 @@ import type { import type { CloudPulseTimeRangeSelectProps } from '../shared/CloudPulseTimeRangeSelect'; import type { CloudPulseMetricsAdditionalFilters } from '../Widget/CloudPulseWidget'; import type { CloudPulseServiceTypeFilters } from './models'; -import type { Dashboard, Filter, Filters, TimeDuration } from '@linode/api-v4'; +import type { + AclpConfig, + Dashboard, + Filter, + Filters, + TimeDuration, +} from '@linode/api-v4'; interface CloudPulseFilterProperties { config: CloudPulseServiceTypeFilters; @@ -21,6 +32,7 @@ interface CloudPulseFilterProperties { [key: string]: FilterValueType; }; isServiceAnalyticsIntegration: boolean; + preferences?: AclpConfig; } interface CloudPulseMandatoryFilterCheckProps { @@ -42,11 +54,12 @@ interface CloudPulseMandatoryFilterCheckProps { */ export const getRegionProperties = ( props: CloudPulseFilterProperties, - handleRegionChange: (region: string | undefined) => void + handleRegionChange: (region: string | undefined, savePref?: boolean) => void ): CloudPulseRegionSelectProps => { const { placeholder } = props.config.configuration; - const { dashboard, isServiceAnalyticsIntegration } = props; + const { dashboard, isServiceAnalyticsIntegration, preferences } = props; return { + defaultValue: preferences?.[REGION], handleRegionChange, placeholder, savePreferences: !isServiceAnalyticsIntegration, @@ -66,7 +79,10 @@ export const getRegionProperties = ( */ export const getResourcesProperties = ( props: CloudPulseFilterProperties, - handleResourceChange: (resourceId: CloudPulseResources[]) => void + handleResourceChange: ( + resourceId: CloudPulseResources[], + savePref?: boolean + ) => void ): CloudPulseResourcesSelectProps => { const { filterKey, placeholder } = props.config.configuration; const { @@ -74,8 +90,10 @@ export const getResourcesProperties = ( dashboard, dependentFilters, isServiceAnalyticsIntegration, + preferences, } = props; return { + defaultValue: preferences?.[RESOURCES], disabled: checkIfWeNeedToDisableFilterByFilterKey( filterKey, dependentFilters ?? {}, @@ -96,7 +114,12 @@ export const getResourcesProperties = ( */ export const getCustomSelectProperties = ( props: CloudPulseFilterProperties, - handleCustomSelectChange: (filterKey: string, value: FilterValueType) => void + handleCustomSelectChange: ( + filterKey: string, + value: FilterValueType, + savePref?: boolean, + updatedPreferenceData?: {} + ) => void ): CloudPulseCustomSelectProps => { const { apiIdField, @@ -109,7 +132,12 @@ export const getCustomSelectProperties = ( options, placeholder, } = props.config.configuration; - const { dashboard, dependentFilters, isServiceAnalyticsIntegration } = props; + const { + dashboard, + dependentFilters, + isServiceAnalyticsIntegration, + preferences, + } = props; return { apiResponseIdField: apiIdField, apiResponseLabelField: apiLabelField, @@ -118,6 +146,7 @@ export const getCustomSelectProperties = ( filterKey, dashboard ), + defaultValue: preferences?.[filterKey], disabled: checkIfWeNeedToDisableFilterByFilterKey( filterKey, dependentFilters ?? {}, @@ -130,6 +159,7 @@ export const getCustomSelectProperties = ( maxSelections, options, placeholder, + preferences, savePreferences: !isServiceAnalyticsIntegration, type: options ? CloudPulseSelectTypes.static @@ -147,11 +177,18 @@ export const getCustomSelectProperties = ( */ export const getTimeDurationProperties = ( props: CloudPulseFilterProperties, - handleTimeRangeChange: (timeDuration: TimeDuration) => void + handleTimeRangeChange: ( + timeDuration: TimeDuration, + timeDurationValue?: string, + savePref?: boolean + ) => void ): CloudPulseTimeRangeSelectProps => { const { placeholder } = props.config.configuration; - const { isServiceAnalyticsIntegration } = props; + const { isServiceAnalyticsIntegration, preferences } = props; + + const timeDuration = preferences?.timeDuration; return { + defaultValue: timeDuration, handleStatsChange: handleTimeRangeChange, placeholder, savePreferences: !isServiceAnalyticsIntegration, @@ -267,9 +304,11 @@ export const getMetricsCallCustomFilters = ( selectedFilters: { [key: string]: FilterValueType; }, - serviceType: string + serviceType?: string ): CloudPulseMetricsAdditionalFilters[] => { - const serviceTypeConfig = FILTER_CONFIG.get(serviceType); + const serviceTypeConfig = serviceType + ? FILTER_CONFIG.get(serviceType) + : undefined; // If configuration exists, filter and map it to the desired CloudPulseMetricsAdditionalFilters format return ( diff --git a/packages/manager/src/features/CloudPulse/Utils/UserPreference.ts b/packages/manager/src/features/CloudPulse/Utils/UserPreference.ts index 326dd5aebb2..18550ae5f25 100644 --- a/packages/manager/src/features/CloudPulse/Utils/UserPreference.ts +++ b/packages/manager/src/features/CloudPulse/Utils/UserPreference.ts @@ -1,86 +1,81 @@ +import { useRef } from 'react'; + import { useMutatePreferences, usePreferences, } from 'src/queries/profile/preferences'; -import { DASHBOARD_ID, TIME_DURATION } from './constants'; +import { DASHBOARD_ID, TIME_DURATION, WIDGETS } from './constants'; import type { AclpConfig, AclpWidget } from '@linode/api-v4'; -let userPreference: AclpConfig; -let timerId: ReturnType; -let mutateFn: any; - -export const useLoadUserPreferences = () => { - const { data: preferences, isError, isLoading } = usePreferences(); - - const { mutate } = useMutatePreferences(); - - if (isLoading) { - return { isLoading }; - } - mutateFn = mutate; - - if (isError || !preferences) { - userPreference = {} as AclpConfig; - } else { - userPreference = preferences.aclpPreference ?? {}; - } - - return { isLoading }; -}; - -export const getUserPreferenceObject = () => { - return { ...userPreference }; -}; - -const useUpdateUserPreference = (updatedData: AclpConfig) => { - if (mutateFn) { - mutateFn({ aclpPreference: updatedData }); - } -}; - -export const updateGlobalFilterPreference = (data: {}) => { - if (!userPreference) { - userPreference = {} as AclpConfig; - } - const keys = Object.keys(data); +interface AclpPreferenceObject { + isLoading: boolean; + preferences: AclpConfig; + updateGlobalFilterPreference: (data: AclpConfig) => void; + updateWidgetPreference: (label: string, data: Partial) => void; +} - if (keys.includes(DASHBOARD_ID)) { - userPreference = { ...data, [TIME_DURATION]: userPreference.timeDuration }; - } else { - userPreference = { ...userPreference, ...data }; - } +export const useAclpPreference = (): AclpPreferenceObject => { + const { data: preferences, isLoading } = usePreferences(); - debounce(userPreference); -}; + const { mutateAsync: updateFunction } = useMutatePreferences(); -export const updateWidgetPreference = ( - label: string, - data: Partial -) => { - if (!userPreference) { - userPreference = {} as AclpConfig; - } + const preferenceRef = useRef(preferences?.aclpPreference ?? {}); - if (!userPreference.widgets) { - userPreference.widgets = {}; + if (preferences?.aclpPreference) { + preferenceRef.current = preferences.aclpPreference; } - - userPreference.widgets[label] = { - ...userPreference.widgets[label], - label, - ...data, + /** + * + * @param data AclpConfig data to be updated in preferences + */ + const updateGlobalFilterPreference = (data: AclpConfig) => { + let currentPreferences = { ...preferenceRef.current }; + const keys = Object.keys(data); + + if (keys.includes(DASHBOARD_ID)) { + currentPreferences = { + ...data, + [TIME_DURATION]: currentPreferences[TIME_DURATION], + [WIDGETS]: {}, + }; + } else { + currentPreferences = { + ...currentPreferences, + ...data, + }; + } + preferenceRef.current = currentPreferences; + updateFunction({ aclpPreference: currentPreferences }); }; - debounce(userPreference); -}; - -// to avoid frequent preference update calls within 500 ms interval -const debounce = (updatedData: AclpConfig) => { - if (timerId) { - clearTimeout(timerId); - } - - timerId = setTimeout(() => useUpdateUserPreference(updatedData), 500); + /** + * + * @param label label of the widget that should be updated + * @param data AclpWidget data for the label that is to be updated in preference + */ + const updateWidgetPreference = (label: string, data: Partial) => { + // sync with latest preferences + const updatedPreferences = { + ...preferenceRef.current, + [WIDGETS]: { + ...(preferenceRef.current.widgets ?? {}), + }, + }; + updatedPreferences.widgets[label] = { + ...updatedPreferences.widgets[label], + label, + ...data, + }; + + preferenceRef.current = updatedPreferences; + updateFunction({ aclpPreference: updatedPreferences }); + }; + return { + isLoading, + preferences: preferences?.aclpPreference ?? {}, + updateGlobalFilterPreference, + updateWidgetPreference, + }; }; diff --git a/packages/manager/src/features/CloudPulse/Utils/constants.ts b/packages/manager/src/features/CloudPulse/Utils/constants.ts index 05b97cc0c6a..c3e31b022d9 100644 --- a/packages/manager/src/features/CloudPulse/Utils/constants.ts +++ b/packages/manager/src/features/CloudPulse/Utils/constants.ts @@ -21,3 +21,5 @@ export const TIME_GRANULARITY = 'timeGranularity'; export const RELATIVE_TIME_DURATION = 'relative_time_duration'; export const RESOURCE_ID = 'resource_id'; + +export const WIDGETS = 'widgets'; diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx index 2eb36a257b8..202d8f3466a 100644 --- a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx @@ -14,10 +14,7 @@ import { import { AGGREGATE_FUNCTION, SIZE, TIME_GRANULARITY } from '../Utils/constants'; import { constructAdditionalRequestFilters } from '../Utils/FilterBuilder'; import { convertValueToUnit, formatToolTip } from '../Utils/unitConversion'; -import { - getUserPreferenceObject, - updateWidgetPreference, -} from '../Utils/UserPreference'; +import { useAclpPreference } from '../Utils/UserPreference'; import { convertStringToCamelCasesWithSpaces } from '../Utils/utils'; import { CloudPulseAggregateFunction } from './components/CloudPulseAggregateFunction'; import { CloudPulseIntervalSelect } from './components/CloudPulseIntervalSelect'; @@ -120,8 +117,8 @@ export interface LegendRow { } export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { + const { updateWidgetPreference: updatePreferences } = useAclpPreference(); const { data: profile } = useProfile(); - const timezone = profile?.timezone ?? DateTime.local().zoneName; const [widget, setWidget] = React.useState({ ...props.widget }); @@ -140,8 +137,8 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { serviceType, timeStamp, unit, + widget: widgetProp, } = props; - const flags = useFlags(); const jweTokenExpiryError = 'Token expired'; @@ -152,7 +149,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { */ const handleZoomToggle = React.useCallback((zoomInValue: boolean) => { if (savePref) { - updateWidgetPreference(widget.label, { + updatePreferences(widget.label, { [SIZE]: zoomInValue ? 12 : 6, }); } @@ -172,20 +169,19 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { const handleAggregateFunctionChange = React.useCallback( (aggregateValue: string) => { // To avoid updation if user again selected the currently selected value from drop down. - if (aggregateValue !== widget.aggregate_function) { - if (savePref) { - updateWidgetPreference(widget.label, { - [AGGREGATE_FUNCTION]: aggregateValue, - }); - } - - setWidget((currentWidget: Widgets) => { - return { - ...currentWidget, - aggregate_function: aggregateValue, - }; + + if (savePref) { + updatePreferences(widget.label, { + [AGGREGATE_FUNCTION]: aggregateValue, }); } + + setWidget((currentWidget: Widgets) => { + return { + ...currentWidget, + aggregate_function: aggregateValue, + }; + }); }, [] ); @@ -196,47 +192,21 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { */ const handleIntervalChange = React.useCallback( (intervalValue: TimeGranularity) => { - if ( - !widget.time_granularity || - intervalValue.unit !== widget.time_granularity.unit || - intervalValue.value !== widget.time_granularity.value - ) { - if (savePref) { - updateWidgetPreference(widget.label, { - [TIME_GRANULARITY]: { ...intervalValue }, - }); - } - - setWidget((currentWidget: Widgets) => { - return { - ...currentWidget, - time_granularity: { ...intervalValue }, - }; + if (savePref) { + updatePreferences(widget.label, { + [TIME_GRANULARITY]: { ...intervalValue }, }); } + + setWidget((currentWidget: Widgets) => { + return { + ...currentWidget, + time_granularity: { ...intervalValue }, + }; + }); }, [] ); - // Update the widget preference if already not present in the preferences - React.useEffect(() => { - if (savePref) { - const widgets = getUserPreferenceObject()?.widgets; - if (!widgets || !widgets[widget.label]) { - updateWidgetPreference(widget.label, { - [AGGREGATE_FUNCTION]: widget.aggregate_function, - [SIZE]: widget.size, - [TIME_GRANULARITY]: widget.time_granularity, - }); - } - } - }, []); - - /** - * - * @param value number value for the tool tip - * @param unit string unit for the tool tip - * @returns formatted string using @value & @unit - */ const { data: metricsList, @@ -289,6 +259,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { } const metricsApiCallError = error?.[0]?.reason; + return ( @@ -315,7 +286,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { > {availableMetrics?.scrape_interval && ( @@ -327,7 +298,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { availableAggregateFunctions={ availableMetrics!.available_aggregate_functions } - defaultAggregateFunction={widget?.aggregate_function} + defaultAggregateFunction={widgetProp?.aggregate_function} onAggregateFuncChange={handleAggregateFunctionChange} /> )} diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx new file mode 100644 index 00000000000..734d20e6822 --- /dev/null +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx @@ -0,0 +1,198 @@ +import { Grid, Paper } from '@mui/material'; +import React from 'react'; + +import CloudPulseIcon from 'src/assets/icons/entityIcons/monitor.svg'; +import { Placeholder } from 'src/components/Placeholder/Placeholder'; + +import { createObjectCopy } from '../Utils/utils'; +import { CloudPulseWidget } from './CloudPulseWidget'; +import { + allIntervalOptions, + getInSeconds, + getIntervalIndex, +} from './components/CloudPulseIntervalSelect'; + +import type { CloudPulseResources } from '../shared/CloudPulseResourcesSelect'; +import type { + CloudPulseMetricsAdditionalFilters, + CloudPulseWidgetProperties, +} from './CloudPulseWidget'; +import type { + AclpConfig, + AvailableMetrics, + Dashboard, + JWEToken, + MetricDefinitions, + TimeDuration, + Widgets, +} from '@linode/api-v4'; + +interface WidgetProps { + additionalFilters?: CloudPulseMetricsAdditionalFilters[]; + dashboard?: Dashboard | undefined; + duration: TimeDuration; + jweToken?: JWEToken | undefined; + manualRefreshTimeStamp?: number; + metricDefinitions: MetricDefinitions | undefined; + preferences?: AclpConfig; + resourceList: CloudPulseResources[] | undefined; + resources: string[]; + savePref?: boolean; +} + +const renderPlaceHolder = (subtitle: string) => { + return ( + + + + + + ); +}; + +export const RenderWidgets = React.memo( + (props: WidgetProps) => { + const { + additionalFilters, + dashboard, + duration, + jweToken, + manualRefreshTimeStamp, + metricDefinitions, + preferences, + resourceList, + resources, + savePref, + } = props; + + const getCloudPulseGraphProperties = ( + widget: Widgets + ): CloudPulseWidgetProperties => { + const graphProp: CloudPulseWidgetProperties = { + additionalFilters, + ariaLabel: widget.label, + authToken: '', + availableMetrics: undefined, + duration, + errorLabel: 'Error While Loading Data', + resourceIds: resources, + resources: [], + serviceType: dashboard?.service_type ?? '', + timeStamp: manualRefreshTimeStamp, + unit: widget.unit ?? '%', + widget: { ...widget }, + }; + if (savePref) { + graphProp.widget = setPreferredWidgetPlan(graphProp.widget); + } + return graphProp; + }; + + const getTimeGranularity = (scrapeInterval: string) => { + const scrapeIntervalValue = getInSeconds(scrapeInterval); + const index = getIntervalIndex(scrapeIntervalValue); + return index < 0 ? allIntervalOptions[0] : allIntervalOptions[index]; + }; + + const setPreferredWidgetPlan = (widgetObj: Widgets): Widgets => { + const widgetPreferences = preferences?.widgets; + const pref = widgetPreferences?.[widgetObj.label]; + if (pref) { + return { + ...widgetObj, + aggregate_function: + pref.aggregateFunction ?? widgetObj.aggregate_function, + size: pref.size ?? widgetObj.size, + time_granularity: { + ...(pref.timeGranularity ?? widgetObj.time_granularity), + }, + }; + } else { + return { + ...widgetObj, + time_granularity: { + label: 'Auto', + unit: 'Auto', + value: -1, + }, + }; + } + }; + + if (!dashboard || !dashboard.widgets?.length) { + return renderPlaceHolder( + 'No visualizations are available at this moment. Create Dashboards to list here.' + ); + } + + if ( + !dashboard.service_type || + !Boolean(resources.length > 0) || + !jweToken?.token || + !Boolean(resourceList?.length) + ) { + return renderPlaceHolder( + 'Select Dashboard, Region and Resource to visualize metrics' + ); + } + + // maintain a copy + const newDashboard: Dashboard = createObjectCopy(dashboard)!; + return ( + + {{ ...newDashboard }.widgets.map((widget, index) => { + // check if widget metric definition is available or not + if (widget) { + // find the metric defintion of the widget label + const availMetrics = metricDefinitions?.data.find( + (availMetrics: AvailableMetrics) => + widget.label === availMetrics.label + ); + const cloudPulseWidgetProperties = getCloudPulseGraphProperties({ + ...widget, + }); + + // metric definition is available but time_granularity is not present + if ( + availMetrics && + !cloudPulseWidgetProperties.widget.time_granularity + ) { + cloudPulseWidgetProperties.widget.time_granularity = getTimeGranularity( + availMetrics.scrape_interval + ); + } + return ( + + ); + } else { + return ; + } + })} + + ); + }, + (oldProps: WidgetProps, newProps: WidgetProps) => { + const keysToCompare: (keyof WidgetProps)[] = [ + 'dashboard', + 'manualRefreshTimeStamp', + 'jweToken', + 'duration', + 'resources', + ]; + + for (const key of keysToCompare) { + if (oldProps[key] !== newProps[key]) { + return false; + } + } + + return true; + } +); diff --git a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.tsx b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.tsx index 3f881d67b32..dba98c8726f 100644 --- a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseAggregateFunction.tsx @@ -16,13 +16,24 @@ export interface AggregateFunctionProperties { /** * Function to be triggered on aggregate function changed from dropdown */ - onAggregateFuncChange: any; + onAggregateFuncChange: (aggregatevalue: string) => void; +} + +interface AggregateFunction { + label: string; + value: string; } export const CloudPulseAggregateFunction = React.memo( (props: AggregateFunctionProperties) => { + const { + availableAggregateFunctions, + defaultAggregateFunction, + onAggregateFuncChange, + } = props; + // Convert list of availableAggregateFunc into a proper response structure accepted by Autocomplete component - const availableAggregateFunc = props.availableAggregateFunctions?.map( + const availableAggregateFunc: AggregateFunction[] = availableAggregateFunctions?.map( (aggrFunc) => { return { label: aggrFunc, @@ -30,30 +41,35 @@ export const CloudPulseAggregateFunction = React.memo( }; } ); - - const defaultAggregateFunc = + const defaultValue = availableAggregateFunc.find( - (obj) => obj.label === props.defaultAggregateFunction - ) || props.availableAggregateFunctions[0]; + (obj) => obj.label === defaultAggregateFunction + ) || availableAggregateFunc[0]; + + const [ + selectedAggregateFunction, + setSelectedAggregateFunction, + ] = React.useState(defaultValue); return ( { return option.label == value.label; }} - onChange={(_: any, selectedAggregateFunc: any) => { - props.onAggregateFuncChange(selectedAggregateFunc.label); + onChange={(e, selectedAggregateFunc: AggregateFunction) => { + setSelectedAggregateFunction(selectedAggregateFunc); + onAggregateFuncChange(selectedAggregateFunc.label); }} textFieldProps={{ hideLabel: true, }} - defaultValue={defaultAggregateFunc} disableClearable fullWidth={false} label="Select an Aggregate Function" noMarginTop={true} options={availableAggregateFunc} sx={{ width: '100%' }} + value={selectedAggregateFunction} /> ); } diff --git a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx index 7370937f1e3..13a5bdee9eb 100644 --- a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx @@ -19,7 +19,7 @@ export interface IntervalSelectProperties { /** * Function to be triggered on aggregate function changed from dropdown */ - onIntervalChange: any; + onIntervalChange: (intervalValue: TimeGranularity) => void; /** * scrape intervalto filter out minimum time granularity @@ -84,7 +84,8 @@ export const getIntervalIndex = (scrapeIntervalValue: number) => { export const CloudPulseIntervalSelect = React.memo( (props: IntervalSelectProperties) => { - const scrapeIntervalValue = getInSeconds(props.scrapeInterval); + const { defaultInterval, onIntervalChange, scrapeInterval } = props; + const scrapeIntervalValue = getInSeconds(scrapeInterval); const firstIntervalIndex = getIntervalIndex(scrapeIntervalValue); @@ -97,22 +98,25 @@ export const CloudPulseIntervalSelect = React.memo( allIntervalOptions.length ); - let default_interval = - props.defaultInterval?.unit === 'Auto' + let defaultValue = + defaultInterval?.unit === 'Auto' ? autoIntervalOption : availableIntervalOptions.find( (obj) => - obj.value === props.defaultInterval?.value && - obj.unit === props.defaultInterval?.unit + obj.value === defaultInterval?.value && + obj.unit === defaultInterval?.unit ); - if (!default_interval) { - default_interval = autoIntervalOption; - props.onIntervalChange({ - unit: default_interval.unit, - value: default_interval.value, + if (!defaultValue) { + defaultValue = autoIntervalOption; + onIntervalChange({ + unit: defaultValue.unit, + value: defaultValue.value, }); } + const [selectedInterval, setSelectedInterval] = React.useState( + defaultValue + ); return ( { - props.onIntervalChange({ + setSelectedInterval(selectedInterval); + onIntervalChange({ unit: selectedInterval?.unit, value: selectedInterval?.value, }); @@ -134,13 +139,13 @@ export const CloudPulseIntervalSelect = React.memo( textFieldProps={{ hideLabel: true, }} - defaultValue={{ ...default_interval }} disableClearable fullWidth={false} label="Select an Interval" noMarginTop={true} options={[autoIntervalOption, ...availableIntervalOptions]} sx={{ width: { xs: '100%' } }} + value={selectedInterval} /> ); } diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index 6209063e228..63a98134585 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -1,4 +1,3 @@ -import deepEqual from 'fast-deep-equal'; import React from 'react'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; @@ -14,6 +13,7 @@ import type { CloudPulseServiceTypeFiltersOptions, QueryFunctionAndKey, } from '../Utils/models'; +import type { AclpConfig, FilterValue } from '@linode/api-v4'; /** * These are the properties requires for CloudPulseCustomSelect Components @@ -40,6 +40,11 @@ export interface CloudPulseCustomSelectProps { */ clearDependentSelections?: string[]; + /** + * Last selected values from user preferences + */ + defaultValue?: FilterValue; + /** * This property says, whether or not to disable the selection component */ @@ -54,7 +59,6 @@ export interface CloudPulseCustomSelectProps { * The filterKey that needs to be used */ filterKey: string; - /** * The type of the filter like string, number etc., */ @@ -65,7 +69,13 @@ export interface CloudPulseCustomSelectProps { * @param filterKey - The filterKey of the component * @param value - The selected filter value */ - handleSelectionChange: (filterKey: string, value: FilterValueType) => void; + handleSelectionChange: ( + filterKey: string, + value: FilterValueType, + savePref?: boolean, + updatedPreferenceData?: AclpConfig + ) => void; + /** * If true, multiselect is allowed, otherwise false */ @@ -86,6 +96,8 @@ export interface CloudPulseCustomSelectProps { */ placeholder?: string; + preferences?: AclpConfig; + /** * This property controls whether to save the preferences or not */ @@ -109,6 +121,7 @@ export const CloudPulseCustomSelect = React.memo( apiResponseLabelField, apiV4QueryKey, clearDependentSelections, + defaultValue, disabled, filterKey, handleSelectionChange, @@ -116,6 +129,7 @@ export const CloudPulseCustomSelect = React.memo( maxSelections, options, placeholder, + preferences, savePreferences, type, } = props; @@ -142,10 +156,12 @@ export const CloudPulseCustomSelect = React.memo( if (!selectedResource) { setResource( getInitialDefaultSelections({ + defaultValue, filterKey, handleSelectionChange, isMultiSelect: isMultiSelect ?? false, options: options || queriedResources || [], + preferences, savePreferences: savePreferences ?? false, }) ); @@ -165,6 +181,7 @@ export const CloudPulseCustomSelect = React.memo( filterKey, handleSelectionChange, maxSelections, + savePreferences, value, }); setResource( @@ -242,11 +259,6 @@ function compareProps( } } - // Deep comparison for options - if (!deepEqual(prevProps.options, nextProps.options)) { - return false; - } - // Ignore function props in comparison return true; } diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts index 0c1bc3e1844..d7458ebd729 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.test.ts @@ -5,20 +5,6 @@ import { import type { CloudPulseServiceTypeFiltersOptions } from '../Utils/models'; -const queryMocks = vi.hoisted(() => ({ - getUserPreferenceObject: vi.fn().mockReturnValue({ - test: '1', - }), -})); - -vi.mock('../Utils/UserPreference', async () => { - const actual = await vi.importActual('../Utils/UserPreference'); - return { - ...actual, - getUserPreferenceObject: queryMocks.getUserPreferenceObject, - }; -}); - it('test handleCustomSelectionChange method for single selection', () => { const selectedValue: CloudPulseServiceTypeFiltersOptions = { id: '1', @@ -69,6 +55,7 @@ it('test getInitialDefaultSelections method for single selection', () => { ]; let result = getInitialDefaultSelections({ + defaultValue: '1', filterKey: 'test', handleSelectionChange, isMultiSelect: false, @@ -79,15 +66,15 @@ it('test getInitialDefaultSelections method for single selection', () => { expect(Array.isArray(result)).toBe(false); expect(result).toEqual(options[0]); expect(handleSelectionChange).toBeCalledTimes(1); - queryMocks.getUserPreferenceObject.mockReturnValue({ - test: '2', - }); result = getInitialDefaultSelections({ filterKey: 'test', handleSelectionChange, isMultiSelect: false, options, + preferences: { + test: '2', + }, savePreferences: true, }); expect(result).toEqual(undefined); @@ -97,10 +84,6 @@ it('test getInitialDefaultSelections method for single selection', () => { it('test getInitialDefaultSelections method for multi selection', () => { const handleSelectionChange = vi.fn(); - queryMocks.getUserPreferenceObject.mockReturnValue({ - test: '1', - }); - const options: CloudPulseServiceTypeFiltersOptions[] = [ { id: '1', @@ -109,6 +92,7 @@ it('test getInitialDefaultSelections method for multi selection', () => { ]; let result = getInitialDefaultSelections({ + defaultValue: ['1'], filterKey: 'test', handleSelectionChange, isMultiSelect: true, @@ -119,15 +103,15 @@ it('test getInitialDefaultSelections method for multi selection', () => { expect(Array.isArray(result)).toBe(true); expect(result).toEqual(options); expect(handleSelectionChange).toBeCalledTimes(1); - queryMocks.getUserPreferenceObject.mockReturnValue({ - test: '2', - }); result = getInitialDefaultSelections({ filterKey: 'test', handleSelectionChange, isMultiSelect: false, options, + preferences: { + test: '2', + }, savePreferences: true, }); expect(result).toEqual(undefined); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts index 4bd030e8627..ce6dabc4a02 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelectUtils.ts @@ -1,56 +1,101 @@ -import { - getUserPreferenceObject, - updateGlobalFilterPreference, -} from '../Utils/UserPreference'; - import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; import type { CloudPulseServiceTypeFiltersOptions } from '../Utils/models'; +import type { AclpConfig, FilterValue } from '@linode/api-v4'; + +interface CloudPulseCustomSelectProps { + /** + * The current filter key of the rendered custom select component + */ + filterKey: string; + + /** + * The callback for the selection changes happening in the custom select component + */ + handleSelectionChange: ( + filterKey: string, + value: FilterValueType, + savePref?: boolean, + updatedPreferenceData?: AclpConfig + ) => void; + + /** + * Last selected values from user preference + */ + preferences?: AclpConfig; + + /** + * boolean variable to check whether preferences should be saved or not + */ + savePreferences?: boolean; +} /** * The interface for selecting the default value from the user preferences */ -interface CloudPulseCustomSelectDefaultValueProps { +interface CloudPulseCustomSelectDefaultValueProps + extends CloudPulseCustomSelectProps { /** - * The filter Key of the current rendered custom select component + * Default selected value from the drop down */ - filterKey: string; + defaultValue?: FilterValue; + /** - * The callback for the selection changes happening in the custom select component + * Last selected values from user preference */ - handleSelectionChange: (filterKey: string, value: FilterValueType) => void; + preferences?: AclpConfig; /** - * Indicates whether we need multiselect for the component or not + * boolean variable to check whether preferences should be saved or not */ - isMultiSelect: boolean; + savePreferences?: boolean; +} +/** + * The interface for selecting the default value from the user preferences + */ +interface CloudPulseCustomSelectDefaultValueProps + extends CloudPulseCustomSelectProps { /** - * The current listed options in the custom select component + * Default selected value from the drop down */ - options: CloudPulseServiceTypeFiltersOptions[]; + defaultValue?: FilterValue; + + /** + * Last selected values from user preference + */ + preferences?: AclpConfig; /** - * Indicates whether we need to save preferences or not + * boolean variable to check whether preferences should be saved or not */ - savePreferences: boolean; + savePreferences?: boolean; } /** - * The interface of publishing the selection change and updating the user preferences accordingly + * The interface for selecting the default value from the user preferences */ -interface CloudPulseCustomSelectionChangeProps { +interface CloudPulseCustomSelectDefaultValueProps + extends CloudPulseCustomSelectProps { /** - * The list of filters needs to be cleared on selections + * Indicates whether we need multiselect for the component or not */ - clearSelections: string[]; + isMultiSelect: boolean; + /** - * The current filter key of the rendered custom select component + * The current listed options in the custom select component */ - filterKey: string; + options: CloudPulseServiceTypeFiltersOptions[]; +} + +/** + * The interface of publishing the selection change and updating the user preferences accordingly + */ +interface CloudPulseCustomSelectionChangeProps + extends CloudPulseCustomSelectProps { /** - * The callback for the selection changes happening in the custom select component + * The list of filters needs to be cleared on selections */ - handleSelectionChange: (filterKey: string, value: FilterValueType) => void; + clearSelections: string[]; /** * The maximum number of selections that needs to be allowed @@ -77,6 +122,7 @@ export const getInitialDefaultSelections = ( | CloudPulseServiceTypeFiltersOptions[] | undefined => { const { + defaultValue, filterKey, handleSelectionChange, isMultiSelect, @@ -84,9 +130,6 @@ export const getInitialDefaultSelections = ( savePreferences, } = defaultSelectionProps; - const defaultValue = savePreferences - ? getUserPreferenceObject()[filterKey] - : undefined; if (!options || options.length === 0) { return isMultiSelect ? [] : undefined; } @@ -100,7 +143,6 @@ export const getInitialDefaultSelections = ( ); return initialSelection; } - const selectedValues = options.filter(({ id }) => (Array.isArray(defaultValue) ? defaultValue : [defaultValue]).includes( String(id) @@ -138,6 +180,7 @@ export const handleCustomSelectionChange = ( filterKey, handleSelectionChange, maxSelections, + savePreferences, } = selectionChangeProps; let { value } = selectionChangeProps; @@ -152,20 +195,29 @@ export const handleCustomSelectionChange = ( : String(value.id) : undefined; - // publish the selection change - handleSelectionChange(filterKey, result); + let updatedPreferenceData: AclpConfig = {}; // update the preferences - updateGlobalFilterPreference({ - [filterKey]: result, - }); + if (savePreferences) { + updatedPreferenceData = { [filterKey]: result }; + } // update the clear selections in the preference - if (clearSelections) { - clearSelections.forEach((selection) => - updateGlobalFilterPreference({ [selection]: undefined }) + if (clearSelections && savePreferences) { + clearSelections.forEach( + (selection) => + (updatedPreferenceData = { + ...updatedPreferenceData, + [selection]: undefined, + }) ); } - + // publish the selection change + handleSelectionChange( + filterKey, + result, + savePreferences, + updatedPreferenceData + ); return value; }; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx index d908924c1a8..73bb56016ad 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx @@ -10,9 +10,11 @@ import NullComponent from 'src/components/NullComponent'; import RenderComponent from '../shared/CloudPulseComponentRenderer'; import { + DASHBOARD_ID, REGION, RELATIVE_TIME_DURATION, RESOURCE_ID, + RESOURCES, } from '../Utils/constants'; import { getCustomSelectProperties, @@ -24,7 +26,7 @@ import { FILTER_CONFIG } from '../Utils/FilterConfig'; import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding'; import type { CloudPulseServiceTypeFilters } from '../Utils/models'; import type { CloudPulseResources } from './CloudPulseResourcesSelect'; -import type { Dashboard } from '@linode/api-v4'; +import type { AclpConfig, Dashboard } from '@linode/api-v4'; export interface CloudPulseDashboardFilterBuilderProps { /** @@ -36,12 +38,22 @@ export interface CloudPulseDashboardFilterBuilderProps { /** * all the selection changes in the filter goes through this method */ - emitFilterChange: (filterKey: string, value: FilterValueType) => void; + emitFilterChange: ( + filterKey: string, + value: FilterValueType, + savePref?: boolean, + updatePreferenceData?: {} + ) => void; /** * this will handle the restrictions, if the parent of the component is going to be integrated in service analytics page */ isServiceAnalyticsIntegration: boolean; + + /** + * Last selected values from user preferences + */ + preferences?: AclpConfig; } export const CloudPulseDashboardFilterBuilder = React.memo( @@ -50,6 +62,7 @@ export const CloudPulseDashboardFilterBuilder = React.memo( dashboard, emitFilterChange, isServiceAnalyticsIntegration, + preferences, } = props; const [, setDependentFilters] = React.useState<{ @@ -89,33 +102,68 @@ export const CloudPulseDashboardFilterBuilder = React.memo( ); const emitFilterChangeByFilterKey = React.useCallback( - (filterKey: string, filterValue: FilterValueType) => { - emitFilterChange(filterKey, filterValue); + ( + filterKey: string, + filterValue: FilterValueType, + savePref: boolean = false, + updatedPreferenceData: AclpConfig = {} + ) => { + emitFilterChange( + filterKey, + filterValue, + savePref, + updatedPreferenceData + ); checkAndUpdateDependentFilters(filterKey, filterValue); }, [emitFilterChange, checkAndUpdateDependentFilters] ); const handleResourceChange = React.useCallback( - (resourceId: CloudPulseResources[]) => { + (resourceId: CloudPulseResources[], savePref: boolean = false) => { emitFilterChangeByFilterKey( RESOURCE_ID, - resourceId.map((resource) => resource.id) + resourceId.map((resource) => resource.id), + savePref, + { + [RESOURCES]: resourceId.map((resource: { id: string }) => + String(resource.id) + ), + } ); }, [emitFilterChangeByFilterKey] ); const handleRegionChange = React.useCallback( - (region: string | undefined) => { - emitFilterChangeByFilterKey(REGION, region); + (region: string | undefined, savePref: boolean = false) => { + const updatedPreferenceData = { + [REGION]: region, + [RESOURCES]: undefined, + }; + emitFilterChangeByFilterKey( + REGION, + region, + savePref, + updatedPreferenceData + ); }, [emitFilterChangeByFilterKey] ); const handleCustomSelectChange = React.useCallback( - (filterKey: string, value: FilterValueType) => { - emitFilterChangeByFilterKey(filterKey, value); + ( + filterKey: string, + value: FilterValueType, + savePref: boolean = false, + updatedPreferenceData: {} = {} + ) => { + emitFilterChangeByFilterKey( + filterKey, + value, + savePref, + updatedPreferenceData + ); }, [emitFilterChangeByFilterKey] ); @@ -124,7 +172,12 @@ export const CloudPulseDashboardFilterBuilder = React.memo( (config: CloudPulseServiceTypeFilters) => { if (config.configuration.filterKey === REGION) { return getRegionProperties( - { config, dashboard, isServiceAnalyticsIntegration }, + { + config, + dashboard, + isServiceAnalyticsIntegration, + preferences, + }, handleRegionChange ); } else if (config.configuration.filterKey === RESOURCE_ID) { @@ -134,6 +187,7 @@ export const CloudPulseDashboardFilterBuilder = React.memo( dashboard, dependentFilters: dependentFilterReference.current, isServiceAnalyticsIntegration, + preferences, }, handleResourceChange ); @@ -144,6 +198,7 @@ export const CloudPulseDashboardFilterBuilder = React.memo( dashboard, dependentFilters: dependentFilterReference.current, isServiceAnalyticsIntegration, + preferences, }, handleCustomSelectChange ); @@ -155,6 +210,7 @@ export const CloudPulseDashboardFilterBuilder = React.memo( handleResourceChange, handleCustomSelectChange, isServiceAnalyticsIntegration, + preferences, ] ); @@ -271,5 +327,9 @@ function compareProps( oldProps: CloudPulseDashboardFilterBuilderProps, newProps: CloudPulseDashboardFilterBuilderProps ) { - return oldProps.dashboard?.id === newProps.dashboard?.id; + return ( + oldProps.dashboard?.id === newProps.dashboard?.id && + oldProps.preferences?.[DASHBOARD_ID] === + newProps.preferences?.[DASHBOARD_ID] + ); } diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.test.tsx index 1ff706d78d5..602e96d97aa 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.test.tsx @@ -5,12 +5,9 @@ import { dashboardFactory } from 'src/factories'; import * as utils from 'src/features/CloudPulse/Utils/utils'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { DASHBOARD_ID } from '../Utils/constants'; -import * as preferences from '../Utils/UserPreference'; import { CloudPulseDashboardSelect } from './CloudPulseDashboardSelect'; import type { CloudPulseDashboardSelectProps } from './CloudPulseDashboardSelect'; -import type { AclpConfig } from '@linode/api-v4'; const dashboardLabel = 'Factory Dashboard-1'; const props: CloudPulseDashboardSelectProps = { @@ -89,10 +86,13 @@ describe('CloudPulse Dashboard select', () => { ); }), it('Should select the default value from preferences', () => { - const mockFunction = vi.spyOn(preferences, 'getUserPreferenceObject'); - mockFunction.mockReturnValue({ [DASHBOARD_ID]: 1 } as AclpConfig); - - renderWithTheme(); + renderWithTheme( + + ); expect(screen.getByRole('combobox')).toHaveAttribute( 'value', diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.tsx index a27e9051d87..80ee4f335ad 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardSelect.tsx @@ -6,24 +6,23 @@ import { Typography } from 'src/components/Typography'; import { useCloudPulseDashboardsQuery } from 'src/queries/cloudpulse/dashboards'; import { useCloudPulseServiceTypes } from 'src/queries/cloudpulse/services'; -import { DASHBOARD_ID } from '../Utils/constants'; -import { - getUserPreferenceObject, - updateGlobalFilterPreference, -} from '../Utils/UserPreference'; import { formattedServiceTypes, getAllDashboards } from '../Utils/utils'; -import type { Dashboard } from '@linode/api-v4'; +import type { Dashboard, FilterValue } from '@linode/api-v4'; export interface CloudPulseDashboardSelectProps { + defaultValue?: Partial; handleDashboardChange: ( dashboard: Dashboard | undefined, - isDefault?: boolean + savePref?: boolean ) => void; + savePreferences?: boolean; } export const CloudPulseDashboardSelect = React.memo( (props: CloudPulseDashboardSelectProps) => { + const { defaultValue, handleDashboardChange, savePreferences } = props; + const { data: serviceTypesList, error: serviceTypesError, @@ -63,36 +62,31 @@ export const CloudPulseDashboardSelect = React.memo( // sorts dashboards by service type. Required due to unexpected autocomplete grouping behaviour const getSortedDashboardsList = (options: Dashboard[]): Dashboard[] => { - return options.sort( + return [...options].sort( (a, b) => -b.service_type.localeCompare(a.service_type) ); }; // Once the data is loaded, set the state variable with value stored in preferences React.useEffect(() => { // only call this code when the component is rendered initially - if (dashboardsList.length > 0 && selectedDashboard === undefined) { - const dashboardId = getUserPreferenceObject()?.dashboardId; - - if (dashboardId) { - const dashboard = dashboardsList.find( - (obj: Dashboard) => obj.id === dashboardId - ); - setSelectedDashboard(dashboard); - props.handleDashboardChange(dashboard, true); - } else { - props.handleDashboardChange(undefined, true); - } + if ( + savePreferences && + dashboardsList.length > 0 && + selectedDashboard === undefined + ) { + const dashboard = defaultValue + ? dashboardsList.find((obj: Dashboard) => obj.id === defaultValue) + : undefined; + setSelectedDashboard(dashboard); + handleDashboardChange(dashboard); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [dashboardsList]); return ( { - updateGlobalFilterPreference({ - [DASHBOARD_ID]: dashboard?.id, - }); + onChange={(e, dashboard: Dashboard) => { setSelectedDashboard(dashboard); - props.handleDashboardChange(dashboard); + handleDashboardChange(dashboard, savePreferences); }} renderGroup={(params) => ( diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseErrorPlaceholder.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseErrorPlaceholder.tsx new file mode 100644 index 00000000000..fefa287ee1c --- /dev/null +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseErrorPlaceholder.tsx @@ -0,0 +1,23 @@ +import { Grid, Paper } from '@mui/material'; +import React from 'react'; + +import CloudPulseIcon from 'src/assets/icons/entityIcons/monitor.svg'; +import { StyledPlaceholder } from 'src/features/StackScripts/StackScriptBase/StackScriptBase.styles'; + +export const CloudPulseErrorPlaceholder = React.memo( + (props: { errorMessage: string }) => { + const { errorMessage } = props; + return ( + + + + + + ); + } +); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.tsx index de9c342a17b..7f775d3eb87 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.tsx @@ -3,16 +3,11 @@ import * as React from 'react'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; import { useRegionsQuery } from 'src/queries/regions/regions'; -import { REGION, RESOURCES } from '../Utils/constants'; -import { - getUserPreferenceObject, - updateGlobalFilterPreference, -} from '../Utils/UserPreference'; - -import type { Dashboard } from '@linode/api-v4'; +import type { Dashboard, FilterValue } from '@linode/api-v4'; export interface CloudPulseRegionSelectProps { - handleRegionChange: (region: string | undefined) => void; + defaultValue?: FilterValue; + handleRegionChange: (region: string | undefined, savePref?: boolean) => void; placeholder?: string; savePreferences?: boolean; selectedDashboard: Dashboard | undefined; @@ -22,36 +17,32 @@ export const CloudPulseRegionSelect = React.memo( (props: CloudPulseRegionSelectProps) => { const { data: regions } = useRegionsQuery(); - const { handleRegionChange, placeholder, selectedDashboard } = props; + const { + defaultValue, + handleRegionChange, + placeholder, + savePreferences, + selectedDashboard, + } = props; const [selectedRegion, setSelectedRegion] = React.useState(); - // Once the data is loaded, set the state variable with value stored in preferences React.useEffect(() => { - const defaultRegion = getUserPreferenceObject()?.region; - - if (regions) { - if (defaultRegion) { - const region = regions.find((obj) => obj.id === defaultRegion)?.id; - handleRegionChange(region); - setSelectedRegion(region); - } else { - setSelectedRegion(undefined); - handleRegionChange(undefined); - } + if (regions && savePreferences) { + const region = defaultValue + ? regions.find((regionObj) => regionObj.id === defaultValue)?.id + : undefined; + handleRegionChange(region); + setSelectedRegion(region); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [regions, selectedDashboard]); + }, [regions]); return ( { - updateGlobalFilterPreference({ - [REGION]: region?.id, - [RESOURCES]: undefined, - }); setSelectedRegion(region?.id); - handleRegionChange(region?.id); + handleRegionChange(region?.id, savePreferences); }} textFieldProps={{ hideLabel: true, @@ -67,5 +58,7 @@ export const CloudPulseRegionSelect = React.memo( value={selectedRegion} /> ); - } + }, + (prevProps, nextProps) => + prevProps.selectedDashboard?.id === nextProps.selectedDashboard?.id ); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx index ff06cdcd0fc..019d038369b 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx @@ -4,12 +4,8 @@ import * as React from 'react'; import { linodeFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { RESOURCES } from '../Utils/constants'; -import * as preferences from '../Utils/UserPreference'; import { CloudPulseResourcesSelect } from './CloudPulseResourcesSelect'; -import type { AclpConfig } from '@linode/api-v4'; - const queryMocks = vi.hoisted(() => ({ useResourcesQuery: vi.fn().mockReturnValue({}), })); @@ -174,15 +170,14 @@ describe('CloudPulseResourcesSelect component tests', () => { isLoading: false, status: 'success', }); - vi.spyOn(preferences, 'getUserPreferenceObject').mockReturnValue({ - [RESOURCES]: ['12'], - } as AclpConfig); renderWithTheme( ); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.tsx index adaf971e1ba..c9ad5c13d3f 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.tsx @@ -1,17 +1,10 @@ -import deepEqual from 'fast-deep-equal'; import React from 'react'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { useResourcesQuery } from 'src/queries/cloudpulse/resources'; import { themes } from 'src/utilities/theme'; -import { RESOURCES } from '../Utils/constants'; -import { - getUserPreferenceObject, - updateGlobalFilterPreference, -} from '../Utils/UserPreference'; - -import type { Filter } from '@linode/api-v4'; +import type { Filter, FilterValue } from '@linode/api-v4'; export interface CloudPulseResources { id: string; @@ -20,8 +13,12 @@ export interface CloudPulseResources { } export interface CloudPulseResourcesSelectProps { + defaultValue?: Partial; disabled?: boolean; - handleResourcesSelection: (resources: CloudPulseResources[]) => void; + handleResourcesSelection: ( + resources: CloudPulseResources[], + savePref?: boolean + ) => void; placeholder?: string; region?: string; resourceType: string | undefined; @@ -32,11 +29,13 @@ export interface CloudPulseResourcesSelectProps { export const CloudPulseResourcesSelect = React.memo( (props: CloudPulseResourcesSelectProps) => { const { + defaultValue, disabled, handleResourcesSelection, placeholder, region, resourceType, + savePreferences, xFilter, } = props; @@ -49,46 +48,40 @@ export const CloudPulseResourcesSelect = React.memo( const [selectedResources, setSelectedResources] = React.useState< CloudPulseResources[] - >([]); + >(); - const getResourcesList = (): CloudPulseResources[] => { + const getResourcesList = React.useMemo(() => { return resources && resources.length > 0 ? resources : []; - }; + }, [resources]); // Once the data is loaded, set the state variable with value stored in preferences React.useEffect(() => { - const saveResources = getUserPreferenceObject()?.resources; - const defaultResources = Array.isArray(saveResources) - ? saveResources.map((resourceId) => String(resourceId)) - : undefined; - if (resources) { - if (defaultResources) { - const resource = getResourcesList().filter((resource) => - defaultResources.includes(String(resource.id)) - ); - - handleResourcesSelection(resource); - setSelectedResources(resource); - } else { + if (resources && savePreferences && !selectedResources) { + const defaultResources = + defaultValue && Array.isArray(defaultValue) + ? defaultValue.map((resource) => String(resource)) + : []; + const resource = getResourcesList.filter((resource) => + defaultResources.includes(String(resource.id)) + ); + + handleResourcesSelection(resource); + setSelectedResources(resource); + } else { + if (selectedResources) { setSelectedResources([]); - handleResourcesSelection([]); } - } else { - setSelectedResources([]); + handleResourcesSelection([]); } + // eslint-disable-next-line react-hooks/exhaustive-deps - }, [resources, region, resourceType, xFilter]); + }, [resources, region, xFilter, resourceType]); return ( { - updateGlobalFilterPreference({ - [RESOURCES]: resourceSelections.map((resource: { id: string }) => - String(resource.id) - ), - }); + onChange={(e, resourceSelections: CloudPulseResources[]) => { setSelectedResources(resourceSelections); - handleResourcesSelection(resourceSelections); + handleResourcesSelection(resourceSelections, savePreferences); }} placeholder={ selectedResources?.length ? '' : placeholder || 'Select a Resource' @@ -113,8 +106,8 @@ export const CloudPulseResourcesSelect = React.memo( label="Select Resources" limitTags={2} multiple - options={getResourcesList()} - value={selectedResources} + options={getResourcesList} + value={selectedResources ?? []} /> ); }, @@ -136,9 +129,7 @@ function compareProps( return false; } } - - // Deep comparison for xFilter - if (!deepEqual(prevProps.xFilter, nextProps.xFilter)) { + if (prevProps.xFilter !== nextProps.xFilter) { return false; } diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx index b270743305f..8c288622307 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx @@ -2,13 +2,7 @@ import * as React from 'react'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; -import { TIME_DURATION } from '../Utils/constants'; -import { - getUserPreferenceObject, - updateGlobalFilterPreference, -} from '../Utils/UserPreference'; - -import type { TimeDuration } from '@linode/api-v4'; +import type { FilterValue, TimeDuration } from '@linode/api-v4'; import type { BaseSelectProps, Item, @@ -19,8 +13,12 @@ export interface CloudPulseTimeRangeSelectProps BaseSelectProps, false>, 'defaultValue' | 'onChange' > { - handleStatsChange?: (timeDuration: TimeDuration) => void; - placeholder?: string; + defaultValue?: Partial; + handleStatsChange?: ( + timeDuration: TimeDuration, + timeDurationValue?: string, + savePref?: boolean + ) => void; savePreferences?: boolean; } @@ -38,13 +36,14 @@ export type Labels = export const CloudPulseTimeRangeSelect = React.memo( (props: CloudPulseTimeRangeSelectProps) => { - const { handleStatsChange, placeholder } = props; + const { defaultValue, handleStatsChange, savePreferences } = props; const options = generateSelectOptions(); const getDefaultValue = React.useCallback((): Item => { - const defaultValue = getUserPreferenceObject().timeDuration; - + if (!savePreferences) { + return options[0]; + } return options.find((o) => o.label === defaultValue) || options[0]; - }, [options]); + }, []); const [selectedTimeRange, setSelectedTimeRange] = React.useState< Item >(getDefaultValue()); @@ -53,26 +52,28 @@ export const CloudPulseTimeRangeSelect = React.memo( const item = getDefaultValue(); if (handleStatsChange) { - handleStatsChange(getTimeDurationFromTimeRange(item.value)); + handleStatsChange( + getTimeDurationFromTimeRange(item.value), + item.value, + false + ); } - setSelectedTimeRange(item); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // need to execute only once, during mounting of this component - const handleChange = (item: Item) => { - updateGlobalFilterPreference({ - [TIME_DURATION]: item.value, - }); + setSelectedTimeRange(item); if (handleStatsChange) { - handleStatsChange(getTimeDurationFromTimeRange(item.value)); + handleStatsChange( + getTimeDurationFromTimeRange(item.value), + item.value, + savePreferences + ); } - setSelectedTimeRange(item); // update the state variable to retain latest selections }; - return ( ) => { + onChange={(e, value: Item) => { handleChange(value); }} textFieldProps={{ @@ -85,7 +86,6 @@ export const CloudPulseTimeRangeSelect = React.memo( isOptionEqualToValue={(option, value) => option.value === value.value} label="Select Time Duration" options={options} - placeholder={placeholder ?? 'Select a Time Duration'} value={selectedTimeRange} /> ); diff --git a/packages/manager/src/queries/cloudpulse/metrics.ts b/packages/manager/src/queries/cloudpulse/metrics.ts index d949ed371c4..88b6adf02d9 100644 --- a/packages/manager/src/queries/cloudpulse/metrics.ts +++ b/packages/manager/src/queries/cloudpulse/metrics.ts @@ -84,11 +84,13 @@ export const fetchCloudPulseMetrics = ( Authorization: `Bearer ${token}`, }, method: 'POST', - url: `${readApiEndpoint}${encodeURIComponent(serviceType!)}/metrics`, + url: `https://metrics-query.aclp.linode.com/v1/monitor/services/${encodeURIComponent( + serviceType! + )}/metrics`, }; return axiosInstance .request(config) .then((response) => response.data) - .catch((error) => Promise.reject(error.response.data.errors)); + .catch((error) => Promise.reject(error.response?.data?.errors)); };