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));
};