Skip to content

Commit daa0ebd

Browse files
[DI-27664] - Integrate nodebalancer dashboard for firewall in metrics (#12980)
* [DI-27664] - Integrate nodebalancer dashboard for firewall in metrics * [DI-27664] - Remove unnecessary string conversion * [DI-27664] - Update dimension transformation config for nodebalancer_id, add more tests * [DI-27664] - Update uesresourcesquery, add mapping for firewall entity type * upcoming: [DI-27664] - pr comments * upcoming: [DI-27664] - Pr comments * upcoming: [DI-27664] - Add stricter check * upcoming: [DI-27664] - Update filterkey typo for endpoints * upcoming: [DI-27664] - Add changeset * upcoming: [DI-27664] - Remove unnecessary check * upcoming: [DI-27664] - Remove type assertion, keep filters undefined in case of no parent entities * upcoming: [DI-27664] - Drive id based logic from filterconfig * upcoming: [DI-27664] - Update prop name, add util and unit test * upcoming: [DI-27664] - more updates * upcoming: [DI-27664] - make a union type for entity types * upcoming: [DI-27664] - Remove temporary integration in service page
1 parent 2b40bd7 commit daa0ebd

24 files changed

+462
-57
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+
CloudPulse-Metrics: Update `filterConfig.ts`, `useFirewallFetchOptions.tsx` for firewall-nodebalancer dashboard integration ([#12980](https://github.com/linode/manager/pull/12980))

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
CloudPulseServiceType,
2222
Region,
2323
} from '@linode/api-v4';
24+
import type { AssociatedEntityType } from 'src/features/CloudPulse/shared/types';
2425

2526
export const MULTISELECT_PLACEHOLDER_TEXT = 'Select Values';
2627
export const TEXTFIELD_PLACEHOLDER_TEXT = 'Enter a Value';
@@ -340,6 +341,10 @@ export interface FetchOptions {
340341
}
341342

342343
export interface FetchOptionsProps {
344+
/**
345+
* The type of associated entity to filter on.
346+
*/
347+
associatedEntityType?: AssociatedEntityType;
343348
/**
344349
* The dimension label determines the filtering logic and return type.
345350
*/

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/useFirewallFetchOptions.ts

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { useAllLinodesQuery, useAllVPCsQuery } from '@linode/queries';
1+
import {
2+
useAllLinodesQuery,
3+
useAllNodeBalancersQuery,
4+
useAllVPCsQuery,
5+
} from '@linode/queries';
26
import { useMemo } from 'react';
37

48
import { useResourcesQuery } from 'src/queries/cloudpulse/resources';
@@ -8,6 +12,7 @@ import {
812
getFilteredFirewallParentEntities,
913
getFirewallLinodes,
1014
getLinodeRegions,
15+
getNodebalancerRegions,
1116
getVPCSubnets,
1217
} from './utils';
1318

@@ -21,7 +26,15 @@ import type { Filter } from '@linode/api-v4';
2126
export function useFirewallFetchOptions(
2227
props: FetchOptionsProps
2328
): FetchOptions {
24-
const { dimensionLabel, regions, entities, serviceType, type, scope } = props;
29+
const {
30+
dimensionLabel,
31+
regions,
32+
entities,
33+
serviceType,
34+
type,
35+
scope,
36+
associatedEntityType = 'both',
37+
} = props;
2538

2639
const supportedRegionIds =
2740
(serviceType &&
@@ -54,7 +67,10 @@ export function useFirewallFetchOptions(
5467
isError: isResourcesError,
5568
} = useResourcesQuery(
5669
filterLabels.includes(dimensionLabel ?? ''),
57-
'firewall'
70+
'firewall',
71+
{},
72+
{},
73+
associatedEntityType // To avoid fetching resources for which the associated entity type is not supported
5874
);
5975
// Decide firewall resource IDs based on scope
6076
const filteredFirewallParentEntityIds = useMemo(() => {
@@ -68,14 +84,24 @@ export function useFirewallFetchOptions(
6884
);
6985
}, [scope, firewallResources, entities]);
7086

71-
const idFilter = {
87+
const idFilter: Filter = {
7288
'+or': filteredFirewallParentEntityIds.length
73-
? filteredFirewallParentEntityIds.map((id) => ({ id }))
74-
: [{ id: '' }],
89+
? filteredFirewallParentEntityIds.map(({ id }) => ({ id }))
90+
: undefined,
7591
};
7692

77-
const combinedFilter: Filter = {
78-
'+and': [idFilter, regionFilter].filter(Boolean) as Filter[],
93+
const labelFilter: Filter = {
94+
'+or': filteredFirewallParentEntityIds.length
95+
? filteredFirewallParentEntityIds.map(({ label }) => ({ label }))
96+
: undefined,
97+
};
98+
99+
const combinedFilterLinode: Filter = {
100+
'+and': [idFilter, regionFilter].filter(Boolean),
101+
};
102+
103+
const combinedFilterNodebalancer: Filter = {
104+
'+and': [labelFilter, regionFilter].filter(Boolean),
79105
};
80106

81107
// Fetch all linodes with the combined filter
@@ -85,13 +111,30 @@ export function useFirewallFetchOptions(
85111
isLoading: isLinodesLoading,
86112
} = useAllLinodesQuery(
87113
{},
88-
combinedFilter,
114+
combinedFilterLinode,
89115
serviceType === 'firewall' &&
90116
filterLabels.includes(dimensionLabel ?? '') &&
91117
filteredFirewallParentEntityIds.length > 0 &&
118+
(associatedEntityType === 'linode' || associatedEntityType === 'both') &&
92119
supportedRegionIds?.length > 0
93120
);
94121

122+
// Fetch all nodebalancers with the combined filter
123+
const {
124+
data: nodebalancers,
125+
isError: isNodebalancersError,
126+
isLoading: isNodebalancersLoading,
127+
} = useAllNodeBalancersQuery(
128+
serviceType === 'firewall' &&
129+
filterLabels.includes(dimensionLabel ?? '') &&
130+
filteredFirewallParentEntityIds.length > 0 &&
131+
(associatedEntityType === 'nodebalancer' ||
132+
associatedEntityType === 'both') &&
133+
supportedRegionIds?.length > 0,
134+
{},
135+
combinedFilterNodebalancer
136+
);
137+
95138
// Extract linodes from filtered firewall resources
96139
const firewallLinodes = useMemo(
97140
() => getFirewallLinodes(linodes ?? []),
@@ -104,6 +147,17 @@ export function useFirewallFetchOptions(
104147
[linodes]
105148
);
106149

150+
// Extract unique regions from nodebalancers
151+
const nodebalancerRegions = useMemo(
152+
() => getNodebalancerRegions(nodebalancers ?? []),
153+
[nodebalancers]
154+
);
155+
156+
const allRegions = useMemo(
157+
() => Array.from(new Set([...linodeRegions, ...nodebalancerRegions])),
158+
[linodeRegions, nodebalancerRegions]
159+
);
160+
107161
const {
108162
data: vpcs,
109163
isLoading: isVPCsLoading,
@@ -117,18 +171,29 @@ export function useFirewallFetchOptions(
117171
// Determine what options to return based on the dimension label
118172
switch (dimensionLabel) {
119173
case 'associated_entity_region':
120-
case 'region_id':
121174
return {
122-
values: linodeRegions,
123-
isError: isLinodesError || isResourcesError,
124-
isLoading: isLinodesLoading || isResourcesLoading,
175+
values:
176+
associatedEntityType === 'linode'
177+
? linodeRegions
178+
: associatedEntityType === 'nodebalancer'
179+
? nodebalancerRegions
180+
: allRegions,
181+
isError: isLinodesError || isResourcesError || isNodebalancersError,
182+
isLoading:
183+
isLinodesLoading || isResourcesLoading || isNodebalancersLoading,
125184
};
126185
case 'linode_id':
127186
return {
128187
values: firewallLinodes,
129188
isError: isLinodesError || isResourcesError,
130189
isLoading: isLinodesLoading || isResourcesLoading,
131190
};
191+
case 'region_id':
192+
return {
193+
values: linodeRegions,
194+
isError: isLinodesError || isResourcesError,
195+
isLoading: isLinodesLoading || isResourcesLoading,
196+
};
132197
case 'vpc_subnet_id':
133198
return {
134199
values: vpcSubnets,

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/utils.test.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { linodeFactory } from '@linode/utilities';
1+
import { linodeFactory, nodeBalancerFactory } from '@linode/utilities';
22

33
import { transformDimensionValue } from '../../../Utils/utils';
44
import {
55
getFilteredFirewallParentEntities,
66
getFirewallLinodes,
77
getLinodeRegions,
8+
getNodebalancerRegions,
89
getOperatorGroup,
910
getStaticOptions,
1011
handleValueChange,
@@ -120,16 +121,30 @@ describe('Utils', () => {
120121
entities: { b: 'linode-2' },
121122
label: 'firewall-2',
122123
},
124+
{
125+
id: '3',
126+
entities: { c: 'nodebalancer-1' },
127+
label: 'firewall-3',
128+
},
123129
];
124130

125131
it('should return matched resources by entity IDs', () => {
126132
expect(getFilteredFirewallParentEntities(resources, ['1'])).toEqual([
127-
'a',
133+
{
134+
label: 'linode-1',
135+
id: 'a',
136+
},
137+
]);
138+
expect(getFilteredFirewallParentEntities(resources, ['3'])).toEqual([
139+
{
140+
label: 'nodebalancer-1',
141+
id: 'c',
142+
},
128143
]);
129144
});
130145

131146
it('should return empty array if no match', () => {
132-
expect(getFilteredFirewallParentEntities(resources, ['3'])).toEqual([]);
147+
expect(getFilteredFirewallParentEntities(resources, ['4'])).toEqual([]);
133148
});
134149

135150
it('should handle undefined inputs', () => {
@@ -191,6 +206,35 @@ describe('Utils', () => {
191206
});
192207
});
193208

209+
describe('getNodebalancerRegions', () => {
210+
it('should extract and deduplicate regions', () => {
211+
const nodebalancers = nodeBalancerFactory.buildList(3, {
212+
region: 'us-east',
213+
});
214+
nodebalancers[1].region = 'us-west'; // introduce a second unique region
215+
216+
const result = getNodebalancerRegions(nodebalancers);
217+
expect(result).toEqual([
218+
{
219+
label: transformDimensionValue(
220+
'firewall',
221+
'region_id',
222+
nodebalancers[0].region
223+
),
224+
value: 'us-east',
225+
},
226+
{
227+
label: transformDimensionValue(
228+
'firewall',
229+
'region_id',
230+
nodebalancers[1].region
231+
),
232+
value: 'us-west',
233+
},
234+
]);
235+
});
236+
});
237+
194238
describe('scopeBasedFilteredBuckets', () => {
195239
const buckets: CloudPulseResources[] = [
196240
{ label: 'bucket-1', id: 'bucket-1', region: 'us-east' },

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/utils.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import type {
77
CloudPulseServiceType,
88
DimensionFilterOperatorType,
99
Linode,
10+
NodeBalancer,
1011
VPC,
1112
} from '@linode/api-v4';
1213
import type { CloudPulseResources } from 'src/features/CloudPulse/shared/CloudPulseResourcesSelect';
14+
import type { FirewallEntity } from 'src/features/CloudPulse/shared/types';
1315

1416
/**
1517
* Resolves the selected value(s) for the Autocomplete component from raw string.
@@ -99,13 +101,19 @@ export const getStaticOptions = (
99101
export const getFilteredFirewallParentEntities = (
100102
firewallResources: CloudPulseResources[] | undefined,
101103
entities: string[] | undefined
102-
): string[] => {
104+
): FirewallEntity[] => {
103105
if (!(firewallResources?.length && entities?.length)) return [];
104106

105107
return firewallResources
106108
.filter((firewall) => entities.includes(firewall.id))
107109
.flatMap((firewall) =>
108-
firewall.entities ? Object.keys(firewall.entities) : []
110+
// combine key as id and value as label for each entity
111+
firewall.entities
112+
? Object.entries(firewall.entities).map(([id, label]) => ({
113+
id,
114+
label,
115+
}))
116+
: []
109117
);
110118
};
111119

@@ -139,6 +147,23 @@ export const getLinodeRegions = (linodes: Linode[]): Item<string, string>[] => {
139147
}));
140148
};
141149

150+
/**
151+
* Extracts unique region values from a list of nodebalancers.
152+
* @param nodebalancers - Nodebalancer objects with region information.
153+
* @returns - Deduplicated list of regions as options.
154+
*/
155+
export const getNodebalancerRegions = (
156+
nodebalancers: NodeBalancer[]
157+
): Item<string, string>[] => {
158+
if (!nodebalancers) return [];
159+
const regions = new Set<string>();
160+
nodebalancers.forEach(({ region }) => region && regions.add(region));
161+
return Array.from(regions).map((region) => ({
162+
label: transformDimensionValue('firewall', 'region_id', region),
163+
value: region,
164+
}));
165+
};
166+
142167
/**
143168
*
144169
* @param vpcs List of VPCs

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111

1212
import { RESOURCE_FILTER_MAP } from '../Utils/constants';
1313
import { useAclpPreference } from '../Utils/UserPreference';
14+
import { getAssociatedEntityType } from '../Utils/utils';
1415
import {
1516
renderPlaceHolder,
1617
RenderWidgets,
@@ -111,6 +112,9 @@ export const CloudPulseDashboard = (props: DashboardProperties) => {
111112
isLoading: isDashboardLoading,
112113
} = useCloudPulseDashboardByIdQuery(dashboardId);
113114

115+
// Get the associated entity type for the dashboard
116+
const associatedEntityType = getAssociatedEntityType(dashboardId);
117+
114118
const {
115119
data: resourceList,
116120
isError: isResourcesApiError,
@@ -119,7 +123,8 @@ export const CloudPulseDashboard = (props: DashboardProperties) => {
119123
Boolean(dashboard?.service_type),
120124
dashboard?.service_type,
121125
{},
122-
RESOURCE_FILTER_MAP[dashboard?.service_type ?? ''] ?? {}
126+
RESOURCE_FILTER_MAP[dashboard?.service_type ?? ''] ?? {},
127+
associatedEntityType
123128
);
124129

125130
const {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22

33
import { CloudPulseErrorPlaceholder } from '../shared/CloudPulseErrorPlaceholder';
44
import {
5-
LINODE_REGION,
5+
PARENT_ENTITY_REGION,
66
REFRESH,
77
REGION,
88
RESOURCE_ID,
@@ -64,9 +64,9 @@ export const CloudPulseDashboardRenderer = React.memo(
6464
duration={timeDuration}
6565
groupBy={groupBy}
6666
linodeRegion={
67-
filterValue[LINODE_REGION] &&
68-
typeof filterValue[LINODE_REGION] === 'string'
69-
? (filterValue[LINODE_REGION] as string)
67+
filterValue[PARENT_ENTITY_REGION] &&
68+
typeof filterValue[PARENT_ENTITY_REGION] === 'string'
69+
? (filterValue[PARENT_ENTITY_REGION] as string)
7070
: undefined
7171
}
7272
manualRefreshTimeStamp={

packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,22 @@ describe('CloudPulseDashboardWithFilters component tests', () => {
238238
const startDate = screen.getByText('Start Date');
239239
expect(startDate).toBeInTheDocument();
240240
});
241+
242+
it('renders a CloudPulseDashboardWithFilters component successfully for firewall nodebalancer', () => {
243+
queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({
244+
data: { ...mockDashboard, service_type: 'firewall', id: 8 },
245+
error: false,
246+
isError: false,
247+
isLoading: false,
248+
});
249+
250+
renderWithTheme(
251+
<CloudPulseDashboardWithFilters dashboardId={8} resource={1} />
252+
);
253+
const startDate = screen.getByText('Start Date');
254+
expect(startDate).toBeInTheDocument();
255+
expect(
256+
screen.getByPlaceholderText('Select a NodeBalancer Region')
257+
).toBeVisible();
258+
});
241259
});

0 commit comments

Comments
 (0)