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 `entity_ids` type in `CloudPulseMetricsRequest` for metrics api in endpoints dahsboard ([#13133](https://github.com/linode/manager/pull/13133))
2 changes: 1 addition & 1 deletion packages/api-v4/src/cloudpulse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export interface Metric {
export interface CloudPulseMetricsRequest {
absolute_time_duration: DateTimeWithPreset | undefined;
associated_entity_region?: string;
entity_ids: number[] | string[];
entity_ids: number[] | string[] | undefined;
entity_region?: string;
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-Metrics: Update `FilterConfig.ts` to handle integration of endpoints dashboard for object-storage service in metrics page([#13133](https://github.com/linode/manager/pull/13133))
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export const CloudPulseDashboard = (props: DashboardProperties) => {
} = useCloudPulseJWEtokenQuery(
dashboard?.service_type,
getJweTokenPayload(),
Boolean(resources) && !isDashboardLoading && !isDashboardApiError
!isDashboardLoading && !isDashboardApiError
);

if (isDashboardApiError) {
Expand Down
20 changes: 18 additions & 2 deletions packages/manager/src/features/CloudPulse/GroupBy/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('useGlobalDimensions method test', () => {

it('should return non-empty options and defaultValue if no common dimensions', () => {
queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({
data: dashboardFactory.build(),
data: dashboardFactory.build({ id: 1 }),
isLoading: false,
});
queryMocks.useGetCloudPulseMetricDefinitionsByServiceType.mockReturnValue({
Expand All @@ -106,7 +106,7 @@ describe('useGlobalDimensions method test', () => {

it('should return non-empty options and defaultValue from preferences', () => {
queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({
data: dashboardFactory.build(),
data: dashboardFactory.build({ id: 1 }),
isLoading: false,
});
queryMocks.useGetCloudPulseMetricDefinitionsByServiceType.mockReturnValue({
Expand All @@ -123,6 +123,22 @@ describe('useGlobalDimensions method test', () => {
isLoading: false,
});
});

it('should not return default option in case of endpoints-only dashboard', () => {
queryMocks.useCloudPulseDashboardByIdQuery.mockReturnValue({
data: dashboardFactory.build({ id: 10 }),
isLoading: false,
});
queryMocks.useGetCloudPulseMetricDefinitionsByServiceType.mockReturnValue({
data: {
data: metricDefinitions,
},
isLoading: false,
});
const result = useGlobalDimensions(10, 'objectstorage');
// Verify if options contain the default option - 'entityId' or not
expect(result.options).toEqual([{ label: 'Dim 2', value: 'Dim 2' }]);
});
});

describe('useWidgetDimension method test', () => {
Expand Down
15 changes: 10 additions & 5 deletions packages/manager/src/features/CloudPulse/GroupBy/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { useCloudPulseDashboardByIdQuery } from 'src/queries/cloudpulse/dashboar
import { useGetCloudPulseMetricDefinitionsByServiceType } from 'src/queries/cloudpulse/services';

import { ASSOCIATED_ENTITY_METRIC_MAP } from '../Utils/constants';
import { getAssociatedEntityType } from '../Utils/FilterConfig';
import {
getAssociatedEntityType,
isEndpointsOnlyDashboard,
} from '../Utils/FilterConfig';

import type { GroupByOption } from './CloudPulseGroupByDrawer';
import type {
Expand Down Expand Up @@ -62,10 +65,12 @@ export const useGlobalDimensions = (
metricDefinition?.data ?? [],
dashboard
);
const commonDimensions = [
defaultOption,
...getCommonDimensions(metricDimensions),
];
const baseDimensions = getCommonDimensions(metricDimensions);
const shouldIncludeDefault = !isEndpointsOnlyDashboard(dashboardId ?? 0);

const commonDimensions = shouldIncludeDefault
? [defaultOption, ...baseDimensions]
: baseDimensions;

const commonGroups = getCommonGroups(
preference ? preference : (dashboard?.group_by ?? []),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ describe('getTimeDurationFromPreset method', () => {
expect(result).toEqual([123]);
});

it('should return entity ids for objectstorage service type', () => {
it('should return entity ids for objectstorage buckets dashboard', () => {
const result = getEntityIds(
[{ id: 'bucket-1', label: 'bucket-name-1' }],
['bucket-1'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,9 @@ export const getCloudPulseMetricRequest = (
presetDuration === undefined
? { end: duration.end, start: duration.start }
: undefined,
entity_ids: getEntityIds(resources, entityIds, widget, serviceType),
entity_ids: !entityIds.length
? undefined
: getEntityIds(resources, entityIds, widget, serviceType),
filters: undefined,
group_by: !groupBy?.length ? undefined : groupBy,
relative_time_duration: presetDuration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import { FILTER_CONFIG } from './FilterConfig';
import { CloudPulseAvailableViews, CloudPulseSelectTypes } from './models';
import { deepEqual } from './utils';

import type { CloudPulseEndpoints } from '../shared/CloudPulseEndpointsSelect';
import type { CloudPulseResources } from '../shared/CloudPulseResourcesSelect';
import type { CloudPulseServiceTypeFilters } from './models';

Expand Down Expand Up @@ -686,13 +685,15 @@ describe('filterUsingDependentFilters', () => {
});

describe('filterEndpointsUsingRegion', () => {
const mockData: CloudPulseEndpoints[] = [
const mockData: CloudPulseResources[] = [
{
...objectStorageEndpointsFactory.build({ region: 'us-east' }),
id: 'us-east-1.linodeobjects.com',
label: 'us-east-1.linodeobjects.com',
},
{
...objectStorageEndpointsFactory.build({ region: 'us-west' }),
id: 'us-west-1.linodeobjects.com',
label: 'us-west-1.linodeobjects.com',
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type {
} from '../Dashboard/CloudPulseDashboardLanding';
import type { CloudPulseCustomSelectProps } from '../shared/CloudPulseCustomSelect';
import type { CloudPulseEndpointsSelectProps } from '../shared/CloudPulseEndpointsSelect';
import type { CloudPulseEndpoints } from '../shared/CloudPulseEndpointsSelect';
import type {
CloudPulseFirewallNodebalancersSelectProps,
CloudPulseNodebalancers,
Expand Down Expand Up @@ -404,12 +403,14 @@ export const getEndpointsProperties = (
preferences
),
handleEndpointsSelection: handleEndpointsChange,
dashboardId: dashboard.id,
label,
placeholder,
serviceType: dashboard.service_type,
region: dependentFilters?.[REGION],
savePreferences: !isServiceAnalyticsIntegration,
xFilter: filterBasedOnConfig(config, dependentFilters ?? {}),
hasRestrictedSelections: config.configuration.hasRestrictedSelections,
};
};

Expand Down Expand Up @@ -733,9 +734,9 @@ export const filterUsingDependentFilters = (
* @returns The filtered endpoints
*/
export const filterEndpointsUsingRegion = (
data?: CloudPulseEndpoints[],
data?: CloudPulseResources[],
regionFilter?: CloudPulseMetricsFilter
): CloudPulseEndpoints[] | undefined => {
): CloudPulseResources[] | undefined => {
if (!data) {
return data;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
getAssociatedEntityType,
getResourcesFilterConfig,
isEndpointsOnlyDashboard,
} from './FilterConfig';

describe('getResourcesFilterConfig', () => {
Expand Down Expand Up @@ -34,3 +35,14 @@ describe('getAssociatedEntityType', () => {
expect(getAssociatedEntityType(8)).toBe('nodebalancer');
});
});

describe('isEndpointsOnlyDashboard', () => {
it('should return true when the dashboard is an endpoints only dashboard', () => {
// Dashboard ID 10 is an endpoints only dashboard
expect(isEndpointsOnlyDashboard(10)).toBe(true);
});
it('should return false when the dashboard is not an endpoints only dashboard', () => {
// Dashboard ID 6 is not an endpoints only dashboard, rather a buckets dashboard
expect(isEndpointsOnlyDashboard(6)).toBe(false);
});
});
73 changes: 70 additions & 3 deletions packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import {
RESOURCE_ID,
} from './constants';
import { CloudPulseAvailableViews, CloudPulseSelectTypes } from './models';
import { filterKubernetesClusters } from './utils';
import { filterKubernetesClusters, getValidSortedEndpoints } from './utils';

import type { AssociatedEntityType } from '../shared/types';
import type { CloudPulseServiceTypeFiltersConfiguration } from './models';
import type { CloudPulseServiceTypeFilterMap } from './models';
import type { KubernetesCluster } from '@linode/api-v4';
import type { KubernetesCluster, ObjectStorageBucket } from '@linode/api-v4';

const TIME_DURATION = 'Time Range';

Expand Down Expand Up @@ -420,6 +420,8 @@ export const OBJECTSTORAGE_CONFIG_BUCKET: Readonly<CloudPulseServiceTypeFilterMa
name: 'Endpoints',
priority: 2,
neededInViews: [CloudPulseAvailableViews.central],
filterFn: (resources: ObjectStorageBucket[]) =>
getValidSortedEndpoints(resources),
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.

Moved the filtering logic from component to config level as it will be used at multiple places and is easier to handle. This function collects endpoints from available buckets.

},
name: 'Endpoints',
},
Expand All @@ -442,6 +444,45 @@ export const OBJECTSTORAGE_CONFIG_BUCKET: Readonly<CloudPulseServiceTypeFilterMa
serviceType: 'objectstorage',
};

export const ENDPOINT_DASHBOARD_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> =
{
capability: capabilityServiceTypeMapping['objectstorage'],
filters: [
{
configuration: {
filterKey: REGION,
children: [ENDPOINT],
filterType: 'string',
isFilterable: true,
isMetricsFilter: true,
name: 'Region',
priority: 1,
neededInViews: [CloudPulseAvailableViews.central],
},
name: 'Region',
},
{
configuration: {
dimensionKey: 'endpoint',
dependency: [REGION],
filterKey: ENDPOINT,
filterType: 'string',
isFilterable: true,
isMetricsFilter: false,
isMultiSelect: true,
hasRestrictedSelections: true,
name: 'Endpoints',
priority: 2,
neededInViews: [CloudPulseAvailableViews.central],
filterFn: (resources: ObjectStorageBucket[]) =>
getValidSortedEndpoints(resources),
},
name: 'Endpoints',
},
],
serviceType: 'objectstorage',
};

export const BLOCKSTORAGE_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
capability: capabilityServiceTypeMapping['blockstorage'],
filters: [
Expand Down Expand Up @@ -522,6 +563,7 @@ export const FILTER_CONFIG: Readonly<
[7, BLOCKSTORAGE_CONFIG],
[8, FIREWALL_NODEBALANCER_CONFIG],
[9, LKE_CONFIG],
[10, ENDPOINT_DASHBOARD_CONFIG],
]);

/**
Expand All @@ -534,8 +576,13 @@ export const getResourcesFilterConfig = (
if (!dashboardId) {
return undefined;
}
// Get the associated entity type for the dashboard
// Get the resources filter configuration for the dashboard
const filterConfig = FILTER_CONFIG.get(dashboardId);
if (isEndpointsOnlyDashboard(dashboardId)) {
return filterConfig?.filters.find(
(filter) => filter.configuration.filterKey === ENDPOINT
)?.configuration;
}
return filterConfig?.filters.find(
(filter) => filter.configuration.filterKey === RESOURCE_ID
)?.configuration;
Expand All @@ -553,3 +600,23 @@ export const getAssociatedEntityType = (
}
return FILTER_CONFIG.get(dashboardId)?.associatedEntityType;
};

/**
* @param dashboardId id of the dashboard
* @returns whether dashboard is an endpoints only dashboard
*/
export const isEndpointsOnlyDashboard = (dashboardId: number): boolean => {
const filterConfig = FILTER_CONFIG.get(dashboardId);
if (!filterConfig) {
return false;
}
const endpointsFilter = filterConfig?.filters.find(
(filter) => filter.name === 'Endpoints'
);
if (endpointsFilter) {
// Verify if the dashboard has buckets filter, if not then it is an endpoints only dashboard
return !filterConfig.filters.some((filter) => filter.name === 'Buckets');
}

return false;
};
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ it('test constructDimensionFilters method', () => {
expect(result[0].filterValue).toEqual('primary');
});

it('test constructDimensionFilters method for endpoints only dashboard', () => {
const result = constructDimensionFilters({
dashboardObj: { ...mockDashboard, id: 10, service_type: 'objectstorage' },
filterValue: {},
resource: 'us-east-1.linodeobjects.com',
groupBy: [],
});
expect(result.length).toEqual(1);
expect(result[0].filterKey).toEqual('endpoint');
expect(result[0].filterValue).toEqual(['us-east-1.linodeobjects.com']);
});

it('test checkIfFilterNeededInMetricsCall method', () => {
let result = checkIfFilterNeededInMetricsCall('region', 2);
expect(result).toEqual(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defaultTimeDuration } from './CloudPulseDateTimePickerUtils';
import { FILTER_CONFIG } from './FilterConfig';
import { ENDPOINT } from './constants';
import { FILTER_CONFIG, isEndpointsOnlyDashboard } from './FilterConfig';
import { CloudPulseAvailableViews } from './models';

import type { DashboardProperties } from '../Dashboard/CloudPulseDashboard';
Expand Down Expand Up @@ -55,7 +56,9 @@ export const getDashboardProperties = (
}),
dashboardId: dashboardObj.id,
duration: timeDuration ?? defaultTimeDuration(),
resources: [String(resource)],
resources: isEndpointsOnlyDashboard(dashboardObj.id)
? []
: [String(resource)],
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.

No entities in case of endpoints dashboard.

serviceType: dashboardObj.service_type,
savePref: false,
groupBy,
Expand Down Expand Up @@ -148,13 +151,20 @@ export const checkIfFilterNeededInMetricsCall = (
export const constructDimensionFilters = (
props: ReusableDashboardFilterUtilProps
): CloudPulseMetricsAdditionalFilters[] => {
const { dashboardObj, filterValue } = props;
return Object.keys(filterValue)
const { dashboardObj, filterValue, resource } = props;
const filters = Object.keys(filterValue)
.filter((key) => checkIfFilterNeededInMetricsCall(key, dashboardObj.id))
.map((key) => ({
filterKey: key,
filterValue: filterValue[key],
}));
if (isEndpointsOnlyDashboard(dashboardObj.id)) {
filters.push({
filterKey: ENDPOINT,
filterValue: [String(resource)],
});
}
return filters;
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.

As endpoints filter needs to go as a dimension filter in filters obj as part of /metrics api call, this needs to be handled for contextual view.

};

/**
Expand Down
Loading