Skip to content

Commit a5e1f48

Browse files
upcoming: [DI-28222] : Integration for widget Level dimension filters in cloudpulse metrics (#13116)
* upcoming: [DI-28222] - Integration changes for dimension filters * upcoming: [DI-28222] - Integration changes for dimension filters * upcoming: [DI-28222] - Fix key issue * upcoming: [DI-28222] - Code refactoring and issue fixes * upcoming: [DI-28222] - Code refactoring * upcoming: [DI-28222] - Add UT for renderers * upcoming: [DI-28222] - Code refactoring to continue support widget filters, incase of widget dimension filters not supported * upcoming: [DI-28222] - UT fix --------- Co-authored-by: agorthi-akamai <[email protected]>
1 parent 2cde3bc commit a5e1f48

20 files changed

+799
-30
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Add integration changes with `CloudPulseWidget` for widget level dimension support in CloudPulse metrics ([#13116](https://github.com/linode/manager/pull/13116))

packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const dashboard = dashboardFactory.build({
104104
widgets: metrics.map(({ name, title, unit, yLabel }) =>
105105
widgetFactory.build({
106106
entity_ids: [String(id)],
107-
filters: [...dimensions],
107+
filters: [],
108108
label: title,
109109
metric: name,
110110
unit,
@@ -367,7 +367,7 @@ describe('Integration Tests for DBaaS Dashboard ', () => {
367367
(filter: DimensionFilter) => filter.dimension_label === 'node_type'
368368
);
369369

370-
expect(nodeTypeFilter).to.have.length(2);
370+
expect(nodeTypeFilter).to.have.length(1);
371371
expect(nodeTypeFilter[0].operator).to.equal('eq');
372372
expect(nodeTypeFilter[0].value).to.equal('secondary');
373373
});
@@ -462,7 +462,7 @@ describe('Integration Tests for DBaaS Dashboard ', () => {
462462
const nodeTypeFilter = filters.filter(
463463
(filter: DimensionFilter) => filter.dimension_label === 'node_type'
464464
);
465-
expect(nodeTypeFilter).to.have.length(2);
465+
expect(nodeTypeFilter).to.have.length(1);
466466
expect(nodeTypeFilter[0].operator).to.equal('eq');
467467
expect(nodeTypeFilter[0].value).to.equal('secondary');
468468

@@ -537,7 +537,7 @@ describe('Integration Tests for DBaaS Dashboard ', () => {
537537
const nodeTypeFilter = filters.filter(
538538
(filter: DimensionFilter) => filter.dimension_label === 'node_type'
539539
);
540-
expect(nodeTypeFilter).to.have.length(2);
540+
expect(nodeTypeFilter).to.have.length(1);
541541
expect(nodeTypeFilter[0].operator).to.equal('eq');
542542
expect(nodeTypeFilter[0].value).to.equal('secondary');
543543
});

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/FirewallDimensionFilterAutocomplete.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@ export const FirewallDimensionFilterAutocomplete = (
3030
scope,
3131
serviceType,
3232
type,
33+
selectedRegions,
3334
} = props;
3435

3536
const { data: regions } = useRegionsQuery();
3637

3738
const { values, isLoading, isError } = useFirewallFetchOptions({
3839
associatedEntityType: entityType,
3940
dimensionLabel,
41+
regions: selectedRegions
42+
? regions?.filter(({ id }) => selectedRegions.includes(id))
43+
: regions,
4044
entities,
41-
regions,
4245
scope,
4346
serviceType,
4447
type,

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/ValueFieldRenderer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export const ValueFieldRenderer = (props: ValueFieldRendererProps) => {
194194
entityType={entityType}
195195
placeholderText={config.placeholder ?? autocompletePlaceholder}
196196
scope={scope}
197+
selectedRegions={selectedRegions}
197198
/>
198199
);
199200
case 'objectstorage':

packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export const DBAAS_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
151151
isMetricsFilter: false, // if it is false, it will go as a part of filter params, else global filter
152152
isMultiSelect: false,
153153
name: 'Node Type',
154+
dimensionKey: 'node_type',
154155
neededInViews: [
155156
CloudPulseAvailableViews.service,
156157
CloudPulseAvailableViews.central,
@@ -203,6 +204,7 @@ export const NODEBALANCER_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
203204
isMetricsFilter: false,
204205
isOptional: true,
205206
name: 'Ports',
207+
dimensionKey: 'port',
206208
neededInViews: [
207209
CloudPulseAvailableViews.central,
208210
CloudPulseAvailableViews.service,
@@ -256,6 +258,7 @@ export const FIREWALL_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
256258
isMetricsFilter: true,
257259
isMultiSelect: false,
258260
name: 'Linode Region',
261+
dimensionKey: 'region_id',
259262
neededInViews: [
260263
CloudPulseAvailableViews.central,
261264
CloudPulseAvailableViews.service,
@@ -274,6 +277,7 @@ export const FIREWALL_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
274277
isMultiSelect: true,
275278
name: 'Interface Types',
276279
isOptional: true,
280+
dimensionKey: 'interface_type',
277281
neededInViews: [
278282
CloudPulseAvailableViews.central,
279283
CloudPulseAvailableViews.service,
@@ -302,6 +306,7 @@ export const FIREWALL_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
302306
isMetricsFilter: false,
303307
isOptional: true,
304308
name: 'Interface IDs',
309+
dimensionKey: 'interface_id',
305310
neededInViews: [
306311
CloudPulseAvailableViews.central,
307312
CloudPulseAvailableViews.service,
@@ -359,6 +364,7 @@ export const FIREWALL_NODEBALANCER_CONFIG: Readonly<CloudPulseServiceTypeFilterM
359364
isMetricsFilter: true,
360365
name: 'NodeBalancer Region',
361366
priority: 2,
367+
dimensionKey: 'region_id',
362368
neededInViews: [
363369
CloudPulseAvailableViews.central,
364370
CloudPulseAvailableViews.service,
@@ -377,6 +383,7 @@ export const FIREWALL_NODEBALANCER_CONFIG: Readonly<CloudPulseServiceTypeFilterM
377383
isMultiSelect: true,
378384
isOptional: true,
379385
name: 'NodeBalancers',
386+
dimensionKey: 'nodebalancer_id',
380387
neededInViews: [
381388
CloudPulseAvailableViews.central,
382389
CloudPulseAvailableViews.service,
@@ -419,6 +426,7 @@ export const OBJECTSTORAGE_CONFIG_BUCKET: Readonly<CloudPulseServiceTypeFilterMa
419426
isMultiSelect: true,
420427
name: 'Endpoints',
421428
priority: 2,
429+
dimensionKey: 'endpoint',
422430
neededInViews: [CloudPulseAvailableViews.central],
423431
filterFn: (resources: ObjectStorageBucket[]) =>
424432
getValidSortedEndpoints(resources),

packages/manager/src/features/CloudPulse/Utils/utils.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,10 @@ export const isValidFilter = (
481481
if (!dimension) return false;
482482

483483
const dimensionConfig =
484-
valueFieldConfig[filter.dimension_label] ?? valueFieldConfig['*'];
484+
valueFieldConfig[filter.dimension_label] ??
485+
valueFieldConfig[
486+
!dimension.values || dimension.values.length === 0 ? 'emptyValue' : '*'
487+
];
485488

486489
const dimensionFieldConfig = dimensionConfig[operatorGroup];
487490

@@ -493,11 +496,7 @@ export const isValidFilter = (
493496
String(filter.value ?? ''),
494497
dimensionFieldConfig
495498
);
496-
} else if (
497-
dimensionFieldConfig.type === 'textfield' ||
498-
!dimension.values ||
499-
!dimension.values.length
500-
) {
499+
} else if (dimensionFieldConfig.type === 'textfield') {
501500
return true;
502501
}
503502

packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useProfile } from '@linode/queries';
1+
import { useProfile, useRegionsQuery } from '@linode/queries';
22
import { Box, Paper, Typography } from '@linode/ui';
33
import { GridLegacy, Stack, useTheme } from '@mui/material';
44
import { DateTime } from 'luxon';
@@ -7,6 +7,8 @@ import React from 'react';
77
import { useFlags } from 'src/hooks/useFlags';
88
import { useCloudPulseMetricsQuery } from 'src/queries/cloudpulse/metrics';
99

10+
import { useBlockStorageFetchOptions } from '../Alerts/CreateAlert/Criteria/DimensionFilterValue/useBlockStorageFetchOptions';
11+
import { useFirewallFetchOptions } from '../Alerts/CreateAlert/Criteria/DimensionFilterValue/useFirewallFetchOptions';
1012
import { WidgetFilterGroupByRenderer } from '../GroupBy/WidgetFilterGroupByRenderer';
1113
import {
1214
generateGraphData,
@@ -18,10 +20,17 @@ import {
1820
SIZE,
1921
TIME_GRANULARITY,
2022
} from '../Utils/constants';
21-
import { constructAdditionalRequestFilters } from '../Utils/FilterBuilder';
23+
import {
24+
constructAdditionalRequestFilters,
25+
constructWidgetDimensionFilters,
26+
} from '../Utils/FilterBuilder';
27+
import { FILTER_CONFIG } from '../Utils/FilterConfig';
2228
import { generateCurrentUnit } from '../Utils/unitConversion';
2329
import { useAclpPreference } from '../Utils/UserPreference';
24-
import { convertStringToCamelCasesWithSpaces } from '../Utils/utils';
30+
import {
31+
convertStringToCamelCasesWithSpaces,
32+
getFilteredDimensions,
33+
} from '../Utils/utils';
2534
import { CloudPulseAggregateFunction } from './components/CloudPulseAggregateFunction';
2635
import { CloudPulseIntervalSelect } from './components/CloudPulseIntervalSelect';
2736
import { CloudPulseLineGraph } from './components/CloudPulseLineGraph';
@@ -30,6 +39,7 @@ import { ZoomIcon } from './components/Zoomer';
3039

3140
import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding';
3241
import type { CloudPulseResources } from '../shared/CloudPulseResourcesSelect';
42+
import type { MetricsDimensionFilter } from './components/DimensionFilters/types';
3343
import type {
3444
CloudPulseServiceType,
3545
DateTimeWithPreset,
@@ -183,6 +193,9 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
183193
dashboardId,
184194
region,
185195
} = props;
196+
const [dimensionFilters, setDimensionFilters] = React.useState<
197+
MetricsDimensionFilter[] | undefined
198+
>(widget.filters);
186199

187200
const timezone =
188201
duration.timeZone ?? profile?.timezone ?? DateTime.local().zoneName;
@@ -191,13 +204,93 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
191204
const scaledWidgetUnit = React.useRef(generateCurrentUnit(unit));
192205

193206
const jweTokenExpiryError = 'Token expired';
194-
const filters: Filters[] | undefined =
195-
additionalFilters?.length || widget?.filters?.length
207+
const { data: regions } = useRegionsQuery();
208+
const linodesFetch = useFirewallFetchOptions({
209+
dimensionLabel: 'linode_id',
210+
type: 'metrics',
211+
entities: entityIds,
212+
regions: regions?.filter((region) => region.id === linodeRegion) ?? [],
213+
scope: 'entity',
214+
serviceType,
215+
associatedEntityType: FILTER_CONFIG.get(dashboardId)?.associatedEntityType,
216+
});
217+
const vpcFetch = useFirewallFetchOptions({
218+
dimensionLabel: 'vpc_subnet_id',
219+
type: 'metrics',
220+
entities: entityIds,
221+
regions: regions?.filter((region) => region.id === linodeRegion) ?? [],
222+
scope: 'entity',
223+
serviceType,
224+
associatedEntityType: FILTER_CONFIG.get(dashboardId)?.associatedEntityType,
225+
});
226+
const linodeFromVolumes = useBlockStorageFetchOptions({
227+
entities: entityIds,
228+
dimensionLabel: 'linode_id',
229+
regions: regions?.filter(({ id }) => id === region) ?? [],
230+
type: 'metrics',
231+
scope: 'entity',
232+
serviceType,
233+
});
234+
// Determine which fetch object is relevant for linodes
235+
const activeLinodeFetch =
236+
serviceType === 'blockstorage' ? linodeFromVolumes : linodesFetch;
237+
238+
// Combine loading states
239+
const isLoadingFilters = activeLinodeFetch.isLoading || vpcFetch.isLoading;
240+
241+
const excludeDimensionFilters = React.useMemo(() => {
242+
return (
243+
FILTER_CONFIG.get(dashboardId)
244+
?.filters.filter(
245+
({ configuration }) => configuration.dimensionKey !== undefined
246+
)
247+
.map(({ configuration }) => configuration.dimensionKey) ?? []
248+
);
249+
}, [dashboardId]);
250+
const filteredDimensions = React.useMemo(() => {
251+
return excludeDimensionFilters && excludeDimensionFilters.length > 0
252+
? availableMetrics?.dimensions.filter(
253+
({ dimension_label: dimensionLabel }) =>
254+
!excludeDimensionFilters.includes(dimensionLabel)
255+
)
256+
: availableMetrics?.dimensions;
257+
}, [availableMetrics?.dimensions, excludeDimensionFilters]);
258+
259+
const filteredSelections = React.useMemo(() => {
260+
if (isLoadingFilters || !flags.aclp?.showWidgetDimensionFilters) {
261+
return dimensionFilters ?? [];
262+
}
263+
264+
return getFilteredDimensions({
265+
dimensions: filteredDimensions ?? [],
266+
linodes: activeLinodeFetch,
267+
vpcs: vpcFetch,
268+
dimensionFilters,
269+
});
270+
}, [
271+
activeLinodeFetch,
272+
dimensionFilters,
273+
filteredDimensions,
274+
flags.aclp?.showWidgetDimensionFilters,
275+
isLoadingFilters,
276+
vpcFetch,
277+
]);
278+
279+
const filters: Filters[] | undefined = React.useMemo(() => {
280+
return additionalFilters?.length ||
281+
widget?.filters?.length ||
282+
dimensionFilters?.length
196283
? [
197284
...constructAdditionalRequestFilters(additionalFilters ?? []),
198-
...(widget.filters ?? []),
285+
...[...(constructWidgetDimensionFilters(filteredSelections) ?? [])], // dashboard level filters followed by widget filters
199286
]
200287
: undefined;
288+
}, [
289+
additionalFilters,
290+
widget?.filters?.length,
291+
dimensionFilters?.length,
292+
filteredSelections,
293+
]);
201294

202295
/**
203296
*
@@ -274,6 +367,34 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
274367
},
275368
[]
276369
);
370+
371+
const handleDimensionFiltersChange = React.useCallback(
372+
(selectedFilters: MetricsDimensionFilter[]) => {
373+
if (savePref) {
374+
updatePreferences(widget.label, {
375+
filters: selectedFilters
376+
.map((filter) => {
377+
if (
378+
filter.value !== null &&
379+
filter.dimension_label !== null &&
380+
filter.operator !== null
381+
) {
382+
return {
383+
dimension_label: filter.dimension_label,
384+
operator: filter.operator,
385+
value: filter.value,
386+
};
387+
} else {
388+
return undefined;
389+
}
390+
})
391+
.filter((filter) => filter !== undefined),
392+
});
393+
}
394+
setDimensionFilters(selectedFilters);
395+
},
396+
[savePref, updatePreferences, widget.label]
397+
);
277398
const {
278399
data: metricsList,
279400
error,
@@ -295,6 +416,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
295416
filters, // any additional dimension filters will be constructed and passed here
296417
},
297418
{
419+
isFiltersLoading: isLoadingFilters,
298420
authToken,
299421
isFlags: Boolean(flags && !isJweTokenFetching),
300422
label: widget.label,
@@ -332,6 +454,24 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
332454
const end = DateTime.fromISO(duration.end, { zone: 'GMT' });
333455
const hours = end.diff(start, 'hours').hours;
334456
const tickFormat = hours <= 24 ? 'hh:mm a' : 'LLL dd';
457+
458+
React.useEffect(() => {
459+
if (
460+
filteredSelections.length !== (dimensionFilters?.length ?? 0) &&
461+
!linodesFetch.isLoading &&
462+
!vpcFetch.isLoading &&
463+
!linodeFromVolumes.isLoading
464+
) {
465+
handleDimensionFiltersChange(filteredSelections);
466+
}
467+
}, [
468+
filteredSelections,
469+
dimensionFilters,
470+
handleDimensionFiltersChange,
471+
linodesFetch.isLoading,
472+
vpcFetch.isLoading,
473+
linodeFromVolumes.isLoading,
474+
]);
335475
return (
336476
<GridLegacy container item lg={widget.size} xs={12}>
337477
<Stack
@@ -394,11 +534,12 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
394534
)}
395535
<Box sx={{ display: 'flex', gap: 2 }}>
396536
{flags.aclp?.showWidgetDimensionFilters && (
397-
<CloudPulseDimensionFiltersSelect // upcoming: Need to pass selected dimensions from widget in upcoming PR
398-
dimensionOptions={availableMetrics?.dimensions ?? []}
537+
<CloudPulseDimensionFiltersSelect
538+
dashboardId={dashboardId}
539+
dimensionOptions={filteredDimensions ?? []}
399540
drawerLabel={availableMetrics?.label ?? ''}
400-
handleSelectionChange={() => {}}
401-
selectedDimensions={[]}
541+
handleSelectionChange={handleDimensionFiltersChange}
542+
selectedDimensions={filteredSelections}
402543
selectedEntities={entityIds}
403544
selectedRegions={linodeRegion ? [linodeRegion] : undefined}
404545
serviceType={serviceType}

0 commit comments

Comments
 (0)