Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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-Metrics: Update `CloudPulseServiceType` and `CapabilityServiceTypeMapping` at types.ts for new service - lke ([#13064](https://github.com/linode/manager/pull/13064))
2 changes: 2 additions & 0 deletions packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type CloudPulseServiceType =
| 'dbaas'
| 'firewall'
| 'linode'
| 'lke'
| 'nodebalancer'
| 'objectstorage';
export type AlertClass = 'dedicated' | 'shared';
Expand Down Expand Up @@ -381,6 +382,7 @@ export const capabilityServiceTypeMapping: Record<
firewall: 'Cloud Firewall',
objectstorage: 'Object Storage',
blockstorage: 'Block Storage',
lke: 'Kubernetes',
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

CloudPulse-Metrics: Update `FilterConfig.ts`, add lke service and the associated filters ([#13064](https://github.com/linode/manager/pull/13064))
41 changes: 38 additions & 3 deletions packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import {
RESOURCE_ID,
} from './constants';
import { CloudPulseAvailableViews, CloudPulseSelectTypes } from './models';
import { filterFirewallResources } from './utils';
import { filterFirewallResources, filterKubernetesClusters } from './utils';

import type { CloudPulseServiceTypeFilterMap } from './models';
import type { Firewall } from '@linode/api-v4';
import type { Firewall, KubernetesCluster } from '@linode/api-v4';

const TIME_DURATION = 'Time Range';

Expand Down Expand Up @@ -470,7 +470,41 @@ export const BLOCKSTORAGE_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
],
serviceType: 'blockstorage',
};

export const LKE_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
capability: capabilityServiceTypeMapping['lke'],
filters: [
{
configuration: {
filterKey: 'region',
filterType: 'string',
isFilterable: false,
isMetricsFilter: false,
name: 'Region',
priority: 1,
neededInViews: [CloudPulseAvailableViews.central],
},
name: 'Region',
},
{
configuration: {
dependency: ['region'],
filterKey: 'resource_id',
filterType: 'string',
isFilterable: true,
isMetricsFilter: true,
isMultiSelect: true,
name: 'Clusters',
neededInViews: [CloudPulseAvailableViews.central],
placeholder: 'Select Clusters',
priority: 2,
filterFn: (resources: KubernetesCluster[]) =>
filterKubernetesClusters(resources),
},
name: 'Clusters',
},
],
serviceType: 'lke',
};
export const FILTER_CONFIG: Readonly<
Map<number, CloudPulseServiceTypeFilterMap>
> = new Map([
Expand All @@ -481,4 +515,5 @@ export const FILTER_CONFIG: Readonly<
[6, OBJECTSTORAGE_CONFIG_BUCKET],
[7, BLOCKSTORAGE_CONFIG],
[8, FIREWALL_NODEBALANCER_CONFIG],
[9, LKE_CONFIG],
]);
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const NO_REGION_MESSAGE: Record<number, string> = {
6: 'No Object Storage buckets configured in any region.',
7: 'No volumes configured in any regions.',
8: 'No firewalls configured in any Nodebalancer regions.',
9: 'No LKE clusters configured in any regions.',
};

export const HELPER_TEXT: Record<string, string> = {
Expand Down
3 changes: 2 additions & 1 deletion packages/manager/src/features/CloudPulse/Utils/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
DatabaseInstance,
DatabaseType,
Firewall,
KubernetesCluster,
Linode,
NodeBalancer,
ObjectStorageBucket,
Expand Down Expand Up @@ -59,11 +60,11 @@ export type QueryFunctionType =
| DatabaseInstance[]
| DatabaseType[]
| Firewall[]
| KubernetesCluster[]
| Linode[]
| NodeBalancer[]
| ObjectStorageBucket[]
| Volume[];

/**
* The non array types of QueryFunctionType like DatabaseEngine|DatabaseType
*/
Expand Down
46 changes: 45 additions & 1 deletion packages/manager/src/features/CloudPulse/Utils/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { regionFactory } from '@linode/utilities';
import { describe, expect, it } from 'vitest';

import { serviceTypesFactory } from 'src/factories';
import { kubernetesClusterFactory, serviceTypesFactory } from 'src/factories';
import {
firewallEntityfactory,
firewallFactory,
Expand All @@ -25,6 +25,7 @@ import {
arePortsValid,
areValidInterfaceIds,
filterFirewallResources,
filterKubernetesClusters,
getAssociatedEntityType,
getEnabledServiceTypes,
getFilteredDimensions,
Expand Down Expand Up @@ -452,6 +453,49 @@ describe('getEnabledServiceTypes', () => {
});
});

describe('filterKubernetesClusters', () => {
it('should return the filtered kubernetes clusters for enterprise', () => {
const clusters = [
...kubernetesClusterFactory.buildList(5, { tier: 'standard' }),
...kubernetesClusterFactory.buildList(5, { tier: 'enterprise' }),
];
expect(filterKubernetesClusters(clusters)).toHaveLength(5);
});
it('should return the filtered kubernetes clusters for enterprise sorted by label', () => {
const clusters = [
kubernetesClusterFactory.build({
tier: 'enterprise',
label: 'pl-labkrk-2-redis-cluster',
}),
kubernetesClusterFactory.build({
tier: 'enterprise',
label: 'pl-labkrk-2-mr-api-4',
}),
kubernetesClusterFactory.build({
tier: 'enterprise',
label: 'pl-labkrk-2-alertmanager2',
}),
kubernetesClusterFactory.build({
tier: 'enterprise',
label: 'pl-labkrk-2-alertmanager',
}),
];

expect(filterKubernetesClusters(clusters)[0].label).toBe(
'pl-labkrk-2-alertmanager'
);
expect(filterKubernetesClusters(clusters)[1].label).toBe(
'pl-labkrk-2-alertmanager2'
);
expect(filterKubernetesClusters(clusters)[2].label).toBe(
'pl-labkrk-2-mr-api-4'
);
expect(filterKubernetesClusters(clusters)[3].label).toBe(
'pl-labkrk-2-redis-cluster'
);
});
});

describe('isValidFilter', () => {
const valuedDim: Dimension = {
dimension_label: 'browser',
Expand Down
13 changes: 13 additions & 0 deletions packages/manager/src/features/CloudPulse/Utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
Dimension,
Firewall,
FirewallDeviceEntity,
KubernetesCluster,
MonitoringCapabilities,
ResourcePage,
Service,
Expand Down Expand Up @@ -587,3 +588,15 @@ export const filterFirewallResources = (
})
);
};

/**
* @param clusters The list of kubernetes clusters
* @returns The filtered kubernetes clusters based on the tier
*/
export const filterKubernetesClusters = (
Comment thread
ankita-akamai marked this conversation as resolved.
clusters: KubernetesCluster[]
): KubernetesCluster[] => {
return clusters
.filter(({ tier }) => tier === 'enterprise')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does CloudPulse support API X-Filters? It may be better to X-Filter for enterprise clusters rather than client side filtering. It could help for large customers with tons of clusters.

@ankita-akamai ankita-akamai Nov 11, 2025 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bnussman-akamai It has been recommended from the backend side to not have additional load of api calls on them, passing xfilters lead to lot of new api requests as cache can't be used. Also, there were UX issues due to the increasing number of api calls. Due to this, we refactored existing components to only have offline filtering and following the same in new features to maintain consistency, this was done a few days ago. cc @venkymano-akamai

.sort((a, b) => a.label.localeCompare(b.label));
};
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,21 @@ describe('CloudPulseRegionSelect', () => {
expect(screen.getByText(NO_REGION_MESSAGE[4])).toBeVisible();
});

it('should render a Region Select component with correct info message when no regions are available for lke service type', async () => {
const user = userEvent.setup();
renderWithTheme(
<CloudPulseRegionSelect
{...props}
selectedDashboard={dashboardFactory.build({
service_type: 'lke',
id: 9,
})}
/>
);
await user.click(screen.getByRole('button', { name: 'Open' }));
expect(screen.getByText(NO_REGION_MESSAGE[9])).toBeVisible();
});

it('Should show the correct linode region in the dropdown for firewall service type when savePreferences is true', async () => {
const user = userEvent.setup();
queryMocks.useRegionsQuery.mockReturnValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
} from '../Utils/constants';
import { deepEqual, filterUsingDependentFilters } from '../Utils/FilterBuilder';
import { FILTER_CONFIG } from '../Utils/FilterConfig';
import { getAssociatedEntityType } from '../Utils/utils';
import {
getAssociatedEntityType,
getResourcesFilterConfig,
} from '../Utils/utils';
import { CLOUD_PULSE_TEXT_FIELD_PROPS } from './styles';

import type { Item } from '../Alerts/constants';
Expand Down Expand Up @@ -56,6 +59,11 @@ export const CloudPulseRegionSelect = React.memo(
} = props;

const { data: regions, isError, isLoading } = useRegionsQuery();
// Get the resources filter configuration for the dashboard
const resourcesFilterConfig = getResourcesFilterConfig(
selectedDashboard?.id
);
const filterFn = resourcesFilterConfig?.filterFn;
const {
data: resources,
isError: isResourcesError,
Expand All @@ -66,7 +74,9 @@ export const CloudPulseRegionSelect = React.memo(
{},
{
...(RESOURCE_FILTER_MAP[selectedDashboard?.service_type ?? ''] ?? {}),
}
},
undefined,
filterFn
);

const flags = useFlags();
Expand Down
30 changes: 29 additions & 1 deletion packages/manager/src/mocks/serverHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,13 @@ export const handlers = [

http.get('*/lke/clusters', async () => {
const clusters = kubernetesAPIResponse.buildList(10);
return HttpResponse.json(makeResourcePage(clusters));
const enterpriseClusters = kubernetesAPIResponse.buildList(11, {
tier: 'enterprise',
region: 'ap-west',
});
return HttpResponse.json(
makeResourcePage([...clusters, ...enterpriseClusters])
);
}),
http.get('*/lke/types', async () => {
const lkeTypes = [
Expand Down Expand Up @@ -3160,6 +3166,14 @@ export const handlers = [
regions: 'us-iad,us-east',
alert: serviceAlertFactory.build({ scope: ['entity'] }),
}),
serviceTypesFactory.build({
label: 'LKE Enterprise',
service_type: 'lke',
regions: 'us-iad,us-east',
alert: serviceAlertFactory.build({
scope: ['entity', 'account', 'region'],
}),
}),
],
};

Expand All @@ -3174,6 +3188,7 @@ export const handlers = [
firewall: 'Firewalls',
objectstorage: 'Object Storage',
blockstorage: 'Block Storage',
lke: 'LKE Enterprise',
};
const response = serviceTypesFactory.build({
service_type: `${serviceType}`,
Expand Down Expand Up @@ -3280,6 +3295,16 @@ export const handlers = [
);
}

if (params.serviceType === 'lke') {
response.data.push(
dashboardFactory.build({
id: 9,
label: 'LKE Enterprise Dashboard',
service_type: 'lke',
})
);
}

return HttpResponse.json(response);
}),
http.get(
Expand Down Expand Up @@ -3672,6 +3697,9 @@ export const handlers = [
} else if (id === '8') {
serviceType = 'firewall';
dashboardLabel = 'Firewall Nodebalancer Dashboard';
} else if (id === '9') {
serviceType = 'lke';
dashboardLabel = 'Kubernetes Enterprise Dashboard';
} else {
serviceType = 'linode';
dashboardLabel = 'Linode Service I/O Statistics';
Expand Down
3 changes: 3 additions & 0 deletions packages/manager/src/queries/cloudpulse/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '@linode/queries';
import { createQueryKeys } from '@lukemorales/query-key-factory';

import { kubernetesQueries } from '../kubernetes';
import { objectStorageQueries } from '../object-storage/queries';
import {
getAllBucketsFromEndpoints,
Expand Down Expand Up @@ -128,6 +129,8 @@ export const queryFactory = createQueryKeys(key, {
queryFn: () => getAllLinodesRequest(params, filters), // since we don't have query factory implementation, in linodes.ts, once it is ready we will reuse that, untill then we will use same query keys
queryKey: ['linodes', params, filters],
};
case 'lke':
return kubernetesQueries.lists._ctx.all;
case 'nodebalancer':
return nodebalancerQueries.nodebalancers._ctx.all(params, filters);
case 'objectstorage':
Expand Down
1 change: 1 addition & 0 deletions packages/utilities/src/__data__/regionsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const regions: Region[] = [
'Linodes',
'Managed Databases',
'Block Storage',
'Kubernetes',
],
},
},
Expand Down