Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Upcoming Features
---

CloudPulse: Update cloud pulse metrics request payload type at `types.ts` ([#12704](https://github.com/linode/manager/pull/12704))
1 change: 1 addition & 0 deletions packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export interface Metric {

export interface CloudPulseMetricsRequest {
absolute_time_duration: DateTimeWithPreset | undefined;
associated_entity_region?: string;
entity_ids: number[];
filters?: Filters[];
group_by: string[];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

CloudPulse: Add linode region filter in `filterconfig.ts`, refactor `CloudPulseRegionSelect.tsx`, add `useFetchOptions.ts` hook ([#12704](https://github.com/linode/manager/pull/12704))
45 changes: 45 additions & 0 deletions packages/manager/cypress/support/constants/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,49 @@ export const widgetDetails = {
port: 1,
protocols: ['TCP', 'UDP'],
},
firewall: {
dashboardName: 'Firewall Dashboard',
id: 4,
metrics: [
{
expectedAggregation: 'max',
expectedAggregationArray: ['sum'],
expectedGranularity: '1 hr',
name: 'system_cpu_utilization_percent',
title: 'CPU Utilization',
unit: '%',
yLabel: 'system_cpu_utilization_ratio',
},
{
expectedAggregation: 'max',
expectedAggregationArray: ['sum'],
expectedGranularity: '1 hr',
name: 'system_memory_usage_by_resource',
title: 'Memory Usage',
unit: 'B',
yLabel: 'system_memory_usage_bytes',
},
{
expectedAggregation: 'max',
expectedAggregationArray: ['sum'],
expectedGranularity: '1 hr',
name: 'system_network_io_by_resource',
title: 'Network Traffic',
unit: 'B',
yLabel: 'system_network_io_bytes_total',
},
{
expectedAggregation: 'max',
expectedAggregationArray: ['sum'],
expectedGranularity: '1 hr',
name: 'system_disk_OPS_total',
title: 'Disk I/O',
unit: 'OPS',
yLabel: 'system_disk_operations_total',
},
],
firewalls: 'Firewall-resource',
serviceType: 'firewall',
region: 'Newark',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useAllLinodesQuery } from '@linode/queries';
import { useMemo } from 'react';

import { useResourcesQuery } from 'src/queries/cloudpulse/resources';

import { filterRegionByServiceType } from '../../../Utils/utils';
import {
getFilteredFirewallResources,
getFirewallLinodes,
getLinodeRegions,
} from './utils';

import type { Item } from '../../../constants';
import type { CloudPulseServiceType, Filter, Region } from '@linode/api-v4';

interface FetchOptionsProps {
/**
* The dimension label determines the filtering logic and return type.
*/
dimensionLabel: null | string;
/**
* List of firewall entity IDs to filter on.
*/
entities?: string[];
/**
* List of regions to filter on.
*/
regions?: Region[];
/**
* Service to apply specific transformations to dimension values.
*/
serviceType?: CloudPulseServiceType | null;
/**
* The type of monitoring to filter on.
*/
type: 'alerts' | 'metrics';
}
/**
* Custom hook to return selectable options based on the dimension type.
* Handles fetching and transforming data for edge-cases.
*/
export function useFetchOptions(
props: FetchOptionsProps
): Item<string, string>[] {
const { dimensionLabel, regions, entities, serviceType, type } = props;

const supportedRegionIds =
(serviceType &&
regions &&
filterRegionByServiceType(type, regions, serviceType).map(
({ id }) => id
)) ||
[];

// Create a filter for regions based on suppoerted region IDs
const regionFilter: Filter =
supportedRegionIds && supportedRegionIds.length > 0
? {
'+or': supportedRegionIds.map((regionId) => ({
region: regionId,
})),
}
: {};

const filterLabels: string[] = [
'parent_vm_entity_id',
'region_id',
'associated_entity_region',
];

// Fetch all firewall resources when dimension requires it
const { data: firewallResources } = useResourcesQuery(
filterLabels.includes(dimensionLabel ?? ''),
'firewall'
);

// Filter firewall resources by the given entities list
const filteredFirewallResourcesIds = useMemo(
() => getFilteredFirewallResources(firewallResources, entities),
[firewallResources, entities]
);

const idFilter = filteredFirewallResourcesIds.length
? { '+or': filteredFirewallResourcesIds.map((id) => ({ id })) }
: [];

const combinedFilter: Filter = {
'+and': [idFilter, regionFilter].filter(Boolean) as Filter[],
};
// Fetch all linodes with the combined filter
const { data: linodes } = useAllLinodesQuery(
{},
combinedFilter,
filterLabels.includes(dimensionLabel ?? '') &&
filteredFirewallResourcesIds.length > 0 &&
supportedRegionIds.length > 0
);

// Extract linodes from filtered firewall resources
const firewallLinodes = useMemo(
() => getFirewallLinodes(linodes ?? []),
[linodes]
);

// Extract unique regions from linodes
const linodeRegions = useMemo(
() => getLinodeRegions(linodes ?? []),
[linodes]
);

// Determine what options to return based on the dimension label
switch (dimensionLabel) {
case 'associated_entity_region':
return linodeRegions;
case 'parent_vm_entity_id':
return firewallLinodes;
case 'region_id':
return linodeRegions;
default:
return [];
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { linodeFactory } from '@linode/utilities';

import { transformDimensionValue } from '../../../Utils/utils';
import {
getFilteredFirewallResources,
getFirewallLinodes,
getLinodeRegions,
getOperatorGroup,
getStaticOptions,
handleValueChange,
resolveSelectedValues,
} from './utils';

import type { Linode } from '@linode/api-v4';
import type { CloudPulseResources } from 'src/features/CloudPulse/shared/CloudPulseResourcesSelect';

describe('Utils', () => {
describe('resolveSelectedValues', () => {
const options = [
Expand Down Expand Up @@ -98,4 +106,90 @@ describe('Utils', () => {
expect(getStaticOptions('linode', 'dim', null)).toEqual([]);
});
});

describe('getFilteredFirewallResources', () => {
const resources: CloudPulseResources[] = [
{
id: '1',
entities: { a: 'linode-1' },
label: 'firewall-1',
},
{
id: '2',
entities: { b: 'linode-2' },
label: 'firewall-2',
},
];

it('should return matched resources by entity IDs', () => {
expect(getFilteredFirewallResources(resources, ['1'])).toEqual(['a']);
});

it('should return empty array if no match', () => {
expect(getFilteredFirewallResources(resources, ['3'])).toEqual([]);
});

it('should handle undefined inputs', () => {
expect(getFilteredFirewallResources(undefined, ['1'])).toEqual([]);
expect(getFilteredFirewallResources(resources, undefined)).toEqual([]);
});
});

describe('getFirewallLinodes', () => {
const linodes: Linode[] = linodeFactory.buildList(2);

it('should return linode options with transformed labels', () => {
expect(getFirewallLinodes(linodes)).toEqual([
{
label: transformDimensionValue(
'firewall',
'parent_vm_entity_id',
linodes[0].label
),
value: linodes[0].id.toString(),
},
{
label: transformDimensionValue(
'firewall',
'parent_vm_entity_id',
linodes[1].label
),
value: linodes[1].id.toString(),
},
]);
});

it('should handle empty linode list', () => {
expect(getFirewallLinodes([])).toEqual([]);
});
});

describe('getLinodeRegions', () => {
it('should extract and deduplicate regions', () => {
const linodes = linodeFactory.buildList(3, {
region: 'us-east',
});
linodes[1].region = 'us-west'; // introduce a second unique region

const result = getLinodeRegions(linodes);
expect(result).toEqual([
{
label: transformDimensionValue(
'firewall',
'region_id',
linodes[0].region
),
value: 'us-east',
},
{
label: transformDimensionValue(
'firewall',
'region_id',
linodes[1].region
),
value: 'us-west',
},
]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import type { OperatorGroup } from './constants';
import type {
CloudPulseServiceType,
DimensionFilterOperatorType,
Linode,
} from '@linode/api-v4';
import type { CloudPulseResources } from 'src/features/CloudPulse/shared/CloudPulseResourcesSelect';

/**
* Resolves the selected value(s) for the Autocomplete component from raw string.
Expand Down Expand Up @@ -85,3 +87,56 @@ export const getStaticOptions = (
})) ?? []
);
};

/**
* Filters firewall resources and returns matching entity IDs.
* @param firewallResources - List of firewall resource objects.
* @param entities - List of target firewall entity IDs.
* @returns - Flattened array of matching entity IDs.
*/
export const getFilteredFirewallResources = (
firewallResources: CloudPulseResources[] | undefined,
entities: string[] | undefined
): string[] => {
if (!(firewallResources?.length && entities?.length)) return [];

return firewallResources
.filter((firewall) => entities.includes(firewall.id))
.flatMap((firewall) =>
firewall.entities ? Object.keys(firewall.entities) : []
);
};

/**
* Extracts linode items from firewall resources by merging entities.
* @param resources - List of firewall resources with entity mappings.
* @returns - Flattened list of linode ID/label pairs as options.
*/
export const getFirewallLinodes = (
linodes: Linode[]
): Item<string, string>[] => {
if (!linodes) return [];
return linodes.map((linode) => ({
label: transformDimensionValue(
'firewall',
'parent_vm_entity_id',
linode.label
),
value: String(linode.id),
}));
};

/**
* Extracts unique region values from a list of linodes.
* @param linodes - Linode objects with region information.
* @returns - Deduplicated list of regions as options.
*/
export const getLinodeRegions = (linodes: Linode[]): Item<string, string>[] => {
if (!linodes) return [];
const regions = new Set<string>();
linodes.forEach(({ region }) => region && regions.add(region));
return Array.from(regions).map((region) => ({
label: transformDimensionValue('firewall', 'region_id', region),
value: region,
}));
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export interface DashboardProperties {
*/
duration: DateTimeWithPreset;

/**
* Selected linode region for the dashboard
*/
linodeRegion?: string;

/**
* optional timestamp to pass as react query param to forcefully re-fetch data
*/
Expand Down Expand Up @@ -69,6 +74,7 @@ export const CloudPulseDashboard = (props: DashboardProperties) => {
manualRefreshTimeStamp,
resources,
savePref,
linodeRegion,
} = props;

const { preferences } = useAclpPreference();
Expand Down Expand Up @@ -154,6 +160,7 @@ export const CloudPulseDashboard = (props: DashboardProperties) => {
duration={duration}
isJweTokenFetching={isJweTokenFetching}
jweToken={jweToken}
linodeRegion={linodeRegion}
manualRefreshTimeStamp={manualRefreshTimeStamp}
metricDefinitions={metricDefinitions}
preferences={preferences}
Expand Down
Loading