Skip to content

Commit c1cff6d

Browse files
upcoming: [DI-27803] - Firewall-Nodebalancer dashboard enhancements - filtering of firewalls & single select firewall filter (linode#13014)
* add filtering for firewalls * update comments * upcoming: [DI-27803] - Update util * upcoming: [DI-27803] - Update comments * upcoming: [DI-27803] - Update util and comments * upcoming: [DI-27803] - Update custom-select, config, utils * upcoming: [DI-27803] - Remove type error * upcoming: [DI-27803] - Update comments * upcoming: [DI-27803] - Improve consistency * [DI-27803] - fix linode interface filtering bug * upcoming: [DI-27803] - Add changeset * upcoming: [DI-27803] - Avoid type casting, convert to string array --------- Co-authored-by: Nikhil Agrawal <165884194+nikhagra-akamai@users.noreply.github.com>
1 parent ca47bf5 commit c1cff6d

17 files changed

Lines changed: 261 additions & 44 deletions
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+
CloudPulse-Metrics: Update `FilterConfig.ts` to make firewall a single-select filter and to filter firewalls based on dashboard ([#13014](https://github.com/linode/manager/pull/13014))

packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111

1212
import { RESOURCE_FILTER_MAP } from '../Utils/constants';
1313
import { useAclpPreference } from '../Utils/UserPreference';
14-
import { getAssociatedEntityType } from '../Utils/utils';
14+
import { getResourcesFilterConfig } from '../Utils/utils';
1515
import {
1616
renderPlaceHolder,
1717
RenderWidgets,
@@ -112,8 +112,11 @@ export const CloudPulseDashboard = (props: DashboardProperties) => {
112112
isLoading: isDashboardLoading,
113113
} = useCloudPulseDashboardByIdQuery(dashboardId);
114114

115-
// Get the associated entity type for the dashboard
116-
const associatedEntityType = getAssociatedEntityType(dashboardId);
115+
// Get the resources filter configuration for the dashboard
116+
const resourcesFilterConfig = getResourcesFilterConfig(dashboardId);
117+
const associatedEntityType =
118+
resourcesFilterConfig?.associatedEntityType ?? 'both';
119+
const filterFn = resourcesFilterConfig?.filterFn;
117120

118121
const {
119122
data: resourceList,
@@ -124,7 +127,8 @@ export const CloudPulseDashboard = (props: DashboardProperties) => {
124127
dashboard?.service_type,
125128
{},
126129
RESOURCE_FILTER_MAP[dashboard?.service_type ?? ''] ?? {},
127-
associatedEntityType
130+
associatedEntityType,
131+
filterFn
128132
);
129133

130134
const {

packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardRenderer.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ export const CloudPulseDashboardRenderer = React.memo(
8080
: undefined
8181
}
8282
resources={
83-
filterValue[RESOURCE_ID] && Array.isArray(filterValue[RESOURCE_ID])
84-
? (filterValue[RESOURCE_ID] as string[])
85-
: []
83+
Array.isArray(filterValue[RESOURCE_ID])
84+
? filterValue[RESOURCE_ID].map(String)
85+
: typeof filterValue[RESOURCE_ID] === 'string'
86+
? [filterValue[RESOURCE_ID]]
87+
: []
8688
}
8789
savePref={true}
8890
serviceType={dashboard.service_type}

packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,37 @@ it('test getResourceSelectionProperties method', () => {
134134
}
135135
});
136136

137+
it('test getResourceSelectionProperties method for linode-firewall', () => {
138+
const resourceSelectionConfig = firewallConfig?.filters.find(
139+
(filterObj) => filterObj.name === 'Firewalls'
140+
);
141+
142+
expect(resourceSelectionConfig).toBeDefined();
143+
144+
if (resourceSelectionConfig) {
145+
const {
146+
disabled,
147+
handleResourcesSelection,
148+
label,
149+
savePreferences,
150+
filterFn,
151+
} = getResourcesProperties(
152+
{
153+
config: resourceSelectionConfig,
154+
dashboard: { ...mockDashboard, id: 4 },
155+
isServiceAnalyticsIntegration: true,
156+
},
157+
vi.fn()
158+
);
159+
const { name } = resourceSelectionConfig.configuration;
160+
expect(handleResourcesSelection).toBeDefined();
161+
expect(savePreferences).toEqual(false);
162+
expect(disabled).toEqual(false);
163+
expect(label).toEqual(name);
164+
expect(filterFn).toBeDefined();
165+
}
166+
});
167+
137168
it('test getResourceSelectionProperties method with disabled true', () => {
138169
const resourceSelectionConfig = linodeConfig?.filters.find(
139170
(filterObj) => filterObj.name === 'Resources'
@@ -349,6 +380,7 @@ it('test getCustomSelectProperties method', () => {
349380
isMultiSelect: isMultiSelectApi,
350381
savePreferences: savePreferencesApi,
351382
type,
383+
filterFn,
352384
} = getCustomSelectProperties(
353385
{
354386
config: customSelectEngineConfig,
@@ -365,6 +397,7 @@ it('test getCustomSelectProperties method', () => {
365397
expect(savePreferencesApi).toEqual(false);
366398
expect(isMultiSelectApi).toEqual(true);
367399
expect(label).toEqual(name);
400+
expect(filterFn).not.toBeDefined();
368401
}
369402
});
370403

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ export const getResourcesProperties = (
183183
resourceType: dashboard.service_type,
184184
savePreferences: !isServiceAnalyticsIntegration,
185185
xFilter: filterBasedOnConfig(config, dependentFilters ?? {}),
186+
associatedEntityType: config.configuration.associatedEntityType ?? 'both',
187+
filterFn: config.configuration.filterFn,
186188
};
187189
};
188190

@@ -247,6 +249,7 @@ export const getCustomSelectProperties = (
247249
options,
248250
placeholder,
249251
isOptional,
252+
filterFn,
250253
} = props.config.configuration;
251254
const {
252255
dashboard,
@@ -285,6 +288,7 @@ export const getCustomSelectProperties = (
285288
type: options
286289
? CloudPulseSelectTypes.static
287290
: CloudPulseSelectTypes.dynamic,
291+
filterFn,
288292
};
289293
};
290294

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { capabilityServiceTypeMapping } from '@linode/api-v4';
22

3+
import { queryFactory } from 'src/queries/cloudpulse/queries';
4+
35
import {
46
ENDPOINT,
57
INTERFACE_IDS_PLACEHOLDER_TEXT,
@@ -8,8 +10,10 @@ import {
810
RESOURCE_ID,
911
} from './constants';
1012
import { CloudPulseAvailableViews, CloudPulseSelectTypes } from './models';
13+
import { filterFirewallResources } from './utils';
1114

1215
import type { CloudPulseServiceTypeFilterMap } from './models';
16+
import type { Firewall } from '@linode/api-v4';
1317

1418
const TIME_DURATION = 'Time Range';
1519

@@ -233,6 +237,8 @@ export const FIREWALL_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
233237
placeholder: 'Select Firewalls',
234238
priority: 1,
235239
associatedEntityType: 'linode',
240+
filterFn: (resources: Firewall[]) =>
241+
filterFirewallResources(resources, 'linode'),
236242
},
237243
name: 'Firewalls',
238244
},
@@ -328,14 +334,16 @@ export const FIREWALL_NODEBALANCER_CONFIG: Readonly<CloudPulseServiceTypeFilterM
328334
filterType: 'string',
329335
isFilterable: true,
330336
isMetricsFilter: true,
331-
isMultiSelect: true,
332-
name: 'Firewalls',
337+
name: 'Firewall',
333338
neededInViews: [CloudPulseAvailableViews.central],
334339
associatedEntityType: 'nodebalancer',
335-
placeholder: 'Select Firewalls',
340+
placeholder: 'Select a Firewall',
336341
priority: 1,
342+
apiV4QueryKey: queryFactory.resources('firewall'),
343+
filterFn: (resources: Firewall[]) =>
344+
filterFirewallResources(resources, 'nodebalancer'),
337345
},
338-
name: 'Firewalls',
346+
name: 'Firewall',
339347
},
340348
{
341349
configuration: {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export const PORT = 'port';
4646

4747
export const INTERFACE_ID = 'interface_id';
4848

49+
export const FIREWALL = 'Firewall';
50+
4951
export const PORTS_HELPER_TEXT =
5052
'Enter one or more port numbers (1-65535) separated by commas.';
5153

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import type {
33
Capabilities,
44
CloudPulseServiceType,
55
DatabaseEngine,
6+
DatabaseInstance,
67
DatabaseType,
8+
Firewall,
9+
Linode,
10+
NodeBalancer,
11+
ObjectStorageBucket,
12+
Volume,
713
} from '@linode/api-v4';
814
import type { QueryFunction, QueryKey } from '@tanstack/react-query';
915

@@ -46,7 +52,15 @@ export interface CloudPulseServiceTypeFilters {
4652
/**
4753
* As of now, the list of possible custom filters are engine, database type, this union type will be expanded if we start enhancing our custom select config
4854
*/
49-
export type QueryFunctionType = DatabaseEngine[] | DatabaseType[];
55+
export type QueryFunctionType =
56+
| DatabaseEngine[]
57+
| DatabaseInstance[]
58+
| DatabaseType[]
59+
| Firewall[]
60+
| Linode[]
61+
| NodeBalancer[]
62+
| ObjectStorageBucket[]
63+
| Volume[];
5064

5165
/**
5266
* The non array types of QueryFunctionType like DatabaseEngine|DatabaseType
@@ -108,6 +122,11 @@ export interface CloudPulseServiceTypeFiltersConfiguration {
108122
*/
109123
dimensionKey?: string;
110124

125+
/**
126+
* This is an optional field, it is used to filter the resources
127+
*/
128+
filterFn?: (resources: QueryFunctionType) => QueryFunctionType;
129+
111130
/**
112131
* This is the field that will be sent in the metrics api call or xFilter
113132
*/

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

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { regionFactory } from '@linode/utilities';
22
import { describe, expect, it } from 'vitest';
33

44
import { serviceTypesFactory } from 'src/factories';
5+
import {
6+
firewallEntityfactory,
7+
firewallFactory,
8+
} from 'src/factories/firewalls';
59

610
import {
711
INTERFACE_ID,
@@ -20,9 +24,10 @@ import {
2024
import {
2125
arePortsValid,
2226
areValidInterfaceIds,
23-
getAssociatedEntityType,
27+
filterFirewallResources,
2428
getEnabledServiceTypes,
2529
getFilteredDimensions,
30+
getResourcesFilterConfig,
2631
isValidFilter,
2732
isValidPort,
2833
useIsAclpSupportedRegion,
@@ -345,17 +350,89 @@ describe('getEnabledServiceTypes', () => {
345350
expect(result).not.toContain('linode');
346351
});
347352

348-
describe('getAssociatedEntityType', () => {
349-
it('should return both if the dashboard id is not provided', () => {
350-
expect(getAssociatedEntityType(undefined)).toBe('both');
353+
describe('getResourcesFilterConfig', () => {
354+
it('should return undefined if the dashboard id is not provided', () => {
355+
expect(getResourcesFilterConfig(undefined)).toBeUndefined();
356+
});
357+
358+
it('should return the resources filter configuration for the linode-firewalldashboard', () => {
359+
const resourcesFilterConfig = getResourcesFilterConfig(4);
360+
expect(resourcesFilterConfig).toBeDefined();
361+
expect(resourcesFilterConfig?.associatedEntityType).toBe('linode');
362+
expect(resourcesFilterConfig?.filterFn).toBeDefined();
351363
});
352364

353-
it('should return the associated entity type for linode firewall dashboard', () => {
354-
expect(getAssociatedEntityType(4)).toBe('linode');
365+
it('should return the resources filter configuration for the nodebalancer-firewall dashboard', () => {
366+
const resourcesFilterConfig = getResourcesFilterConfig(8);
367+
expect(resourcesFilterConfig).toBeDefined();
368+
expect(resourcesFilterConfig?.associatedEntityType).toBe('nodebalancer');
369+
expect(resourcesFilterConfig?.filterFn).toBeDefined();
355370
});
371+
});
356372

357-
it('should return the associated entity type for nodebalancer firewall dashboard', () => {
358-
expect(getAssociatedEntityType(8)).toBe('nodebalancer');
373+
describe('filterFirewallResources', () => {
374+
it('should return the filtered firewall resources for linode', () => {
375+
const resources = [
376+
firewallFactory.build({
377+
entities: [
378+
firewallEntityfactory.build({
379+
id: 1,
380+
label: 'linode-1',
381+
type: 'linode',
382+
}),
383+
],
384+
}),
385+
firewallFactory.build({
386+
entities: [
387+
firewallEntityfactory.build({
388+
id: 33,
389+
label: null,
390+
type: 'linode_interface',
391+
parent_entity: {
392+
id: 2,
393+
label: 'linode-2',
394+
type: 'linode',
395+
},
396+
}),
397+
],
398+
}),
399+
firewallFactory.build({
400+
entities: [
401+
firewallEntityfactory.build({
402+
id: 3,
403+
label: null,
404+
type: 'linode',
405+
}),
406+
],
407+
}),
408+
firewallFactory.build({
409+
entities: [
410+
firewallEntityfactory.build({
411+
id: 4,
412+
label: null,
413+
type: 'linode_interface',
414+
parent_entity: {
415+
id: 3,
416+
label: null,
417+
type: 'linode',
418+
},
419+
}),
420+
],
421+
}),
422+
firewallFactory.build({
423+
entities: [
424+
firewallEntityfactory.build({
425+
id: 2,
426+
label: 'nodebalancer-1',
427+
type: 'nodebalancer',
428+
}),
429+
],
430+
}),
431+
];
432+
expect(filterFirewallResources(resources, 'linode')).toEqual([
433+
resources[0],
434+
resources[1],
435+
]);
359436
});
360437
});
361438
});

0 commit comments

Comments
 (0)