diff --git a/packages/api-v4/.changeset/pr-12912-changed-1758782562755.md b/packages/api-v4/.changeset/pr-12912-changed-1758782562755.md new file mode 100644 index 00000000000..a169768c34a --- /dev/null +++ b/packages/api-v4/.changeset/pr-12912-changed-1758782562755.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +CloudPulse-Metrics: Update `CloudPulseMetricsRequest` and `JWETokenPayLoad` type at `types.ts` ([#12912](https://github.com/linode/manager/pull/12912)) diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index 494e4e2ee21..110fea9476a 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -9,7 +9,6 @@ export type CloudPulseServiceType = | 'linode' | 'nodebalancer' | 'objectstorage'; - export type AlertClass = 'dedicated' | 'shared'; export type DimensionFilterOperatorType = | 'endswith' @@ -134,7 +133,7 @@ export interface Dimension { } export interface JWETokenPayLoad { - entity_ids: number[]; + entity_ids?: number[]; } export interface JWEToken { @@ -149,7 +148,8 @@ export interface Metric { export interface CloudPulseMetricsRequest { absolute_time_duration: DateTimeWithPreset | undefined; associated_entity_region?: string; - entity_ids: number[]; + entity_ids: number[] | string[]; + entity_region?: string; filters?: Filters[]; group_by?: string[]; metrics: Metric[]; diff --git a/packages/manager/.changeset/pr-12912-upcoming-features-1758782466180.md b/packages/manager/.changeset/pr-12912-upcoming-features-1758782466180.md new file mode 100644 index 00000000000..f7705437af0 --- /dev/null +++ b/packages/manager/.changeset/pr-12912-upcoming-features-1758782466180.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +CloudPulse-Metrics: Handle special conditions for `objectstorage` service addition, add related filters at `FilterConfig.ts`, integrate related component `CloudPulseEndpointsSelect.tsx` ([#12912](https://github.com/linode/manager/pull/12912)) diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx index f9b3c3240ad..3403f984a8a 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx @@ -17,7 +17,11 @@ import { } from '../Widget/CloudPulseWidgetRenderer'; import type { CloudPulseMetricsAdditionalFilters } from '../Widget/CloudPulseWidget'; -import type { DateTimeWithPreset, JWETokenPayLoad } from '@linode/api-v4'; +import type { + CloudPulseServiceType, + DateTimeWithPreset, + JWETokenPayLoad, +} from '@linode/api-v4'; export interface DashboardProperties { /** @@ -65,6 +69,11 @@ export interface DashboardProperties { */ savePref?: boolean; + /** + * Selected service type for the dashboard + */ + serviceType: CloudPulseServiceType; + /** * Selected tags for the dashboard */ @@ -79,12 +88,18 @@ export const CloudPulseDashboard = (props: DashboardProperties) => { manualRefreshTimeStamp, resources, savePref, + serviceType, groupBy, linodeRegion, + region, } = props; const { preferences } = useAclpPreference(); + const getJweTokenPayload = (): JWETokenPayLoad => { + if (serviceType === 'objectstorage') { + return {}; + } return { entity_ids: resources?.map((resource) => Number(resource)) ?? [], }; @@ -169,6 +184,7 @@ export const CloudPulseDashboard = (props: DashboardProperties) => { manualRefreshTimeStamp={manualRefreshTimeStamp} metricDefinitions={metricDefinitions} preferences={preferences} + region={region} resourceList={resourceList} resources={resources} savePref={savePref} diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardRenderer.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardRenderer.tsx index d0868b7b15e..5474c73dcce 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardRenderer.tsx @@ -85,6 +85,7 @@ export const CloudPulseDashboardRenderer = React.memo( : [] } savePref={true} + serviceType={dashboard.service_type} tags={ filterValue[TAGS] && Array.isArray(filterValue[TAGS]) ? (filterValue[TAGS] as string[]) diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx index 26e39a95ed2..e5dbc76f886 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.tsx @@ -28,15 +28,19 @@ export interface CloudPulseDashboardWithFiltersProp { * The id of the dashboard that needs to be rendered */ dashboardId: number; + /** + * The region for which the metrics will be listed + */ + region?: string; /** * The resource id for which the metrics will be listed */ - resource: number; + resource: number | string; } export const CloudPulseDashboardWithFilters = React.memo( (props: CloudPulseDashboardWithFiltersProp) => { - const { dashboardId, resource } = props; + const { dashboardId, resource, region } = props; const { data: dashboard, isError } = useCloudPulseDashboardByIdQuery(dashboardId); const [filterData, setFilterData] = React.useState({ @@ -122,6 +126,7 @@ export const CloudPulseDashboardWithFilters = React.memo( dashboardObj: dashboard, filterValue: filterData.id, resource, + region, timeDuration, groupBy, }); @@ -180,7 +185,13 @@ export const CloudPulseDashboardWithFilters = React.memo( emitFilterChange={onFilterChange} handleToggleAppliedFilter={toggleAppliedFilter} isServiceAnalyticsIntegration - resource_ids={[resource]} + resource_ids={ + dashboard.service_type !== 'objectstorage' + ? typeof resource === 'number' + ? [resource] + : undefined + : undefined + } /> )} { const result = getTimeDurationFromPreset('15min'); expect(result).toBe(undefined); }); + + describe('getEntityIds method', () => { + it('should return entity ids for linode service type', () => { + const result = getEntityIds( + [{ id: '123', label: 'linode-1' }], + ['123'], + widgetFactory.build(), + 'linode' + ); + expect(result).toEqual([123]); + }); + + it('should return entity ids for objectstorage service type', () => { + const result = getEntityIds( + [{ id: 'bucket-1', label: 'bucket-name-1' }], + ['bucket-1'], + widgetFactory.build(), + 'objectstorage' + ); + expect(result).toEqual(['bucket-1']); + }); + }); }); diff --git a/packages/manager/src/features/CloudPulse/Utils/CloudPulseWidgetUtils.ts b/packages/manager/src/features/CloudPulse/Utils/CloudPulseWidgetUtils.ts index 7ebfc02b5c4..15285321937 100644 --- a/packages/manager/src/features/CloudPulse/Utils/CloudPulseWidgetUtils.ts +++ b/packages/manager/src/features/CloudPulse/Utils/CloudPulseWidgetUtils.ts @@ -128,11 +128,21 @@ interface MetricRequestProps { */ linodeRegion?: string; + /** + * selected region for the widget + */ + region?: string; + /** * list of CloudPulse resources available */ resources: CloudPulseResources[]; + /** + * service type of the widget + */ + serviceType: CloudPulseServiceType; + /** * widget filters for metrics data */ @@ -326,18 +336,23 @@ export const generateMaxUnit = ( export const getCloudPulseMetricRequest = ( props: MetricRequestProps ): CloudPulseMetricsRequest => { - const { duration, entityIds, resources, widget, groupBy, linodeRegion } = - props; + const { + duration, + entityIds, + resources, + widget, + groupBy, + linodeRegion, + region, + serviceType, + } = props; const preset = duration.preset; - return { absolute_time_duration: preset !== 'reset' && preset !== 'this month' && preset !== 'last month' ? undefined : { end: duration.end, start: duration.start }, - entity_ids: resources - ? entityIds.map((id) => parseInt(id, 10)) - : widget.entity_ids.map((id) => parseInt(id, 10)), + entity_ids: getEntityIds(resources, entityIds, widget, serviceType), filters: undefined, group_by: !groupBy?.length ? undefined : groupBy, relative_time_duration: getTimeDurationFromPreset(preset), @@ -355,9 +370,31 @@ export const getCloudPulseMetricRequest = ( value: widget.time_granularity.value, }, associated_entity_region: linodeRegion, + entity_region: serviceType === 'objectstorage' ? region : undefined, }; }; +/** + * + * @param resources list of CloudPulse resources + * @param entityIds list of entity ids + * @param widget widget + * @returns transformed entity ids + */ +export const getEntityIds = ( + resources: CloudPulseResources[], + entityIds: string[], + widget: Widgets, + serviceType: CloudPulseServiceType +) => { + if (serviceType === 'objectstorage') { + return entityIds; + } + return resources + ? entityIds.map((id) => parseInt(id, 10)) + : widget.entity_ids.map((id) => parseInt(id, 10)); +}; + /** * * @returns generated label name for graph dimension diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts index 3d9b0bf6633..c4fcb62af9a 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.test.ts @@ -13,6 +13,7 @@ import { filterBasedOnConfig, filterEndpointsUsingRegion, filterUsingDependentFilters, + getEndpointsProperties, getFilters, getTextFilterProperties, } from './FilterBuilder'; @@ -46,6 +47,13 @@ const firewallConfig = FILTER_CONFIG.get(4); const dbaasDashboard = dashboardFactory.build({ service_type: 'dbaas', id: 1 }); +const objectStorageBucketDashboard = dashboardFactory.build({ + service_type: 'objectstorage', + id: 6, +}); + +const objectStorageBucketConfig = FILTER_CONFIG.get(6); + it('test getRegionProperties method', () => { const regionConfig = linodeConfig?.filters.find( (filterObj) => filterObj.name === 'Region' @@ -408,6 +416,46 @@ it('test getTextFilterProperties method for interface_id', () => { } }); +it('test getEndpointsProperties method', () => { + const endpointsConfig = objectStorageBucketConfig?.filters.find( + (filterObj) => filterObj.name === 'Endpoints' + ); + + expect(endpointsConfig).toBeDefined(); + + if (endpointsConfig) { + const endpointsProperties = getEndpointsProperties( + { + config: endpointsConfig, + dashboard: objectStorageBucketDashboard, + dependentFilters: { region: 'us-east' }, + isServiceAnalyticsIntegration: false, + }, + vi.fn() + ); + const { + label, + serviceType, + disabled, + savePreferences, + handleEndpointsSelection, + defaultValue, + region, + xFilter, + } = endpointsProperties; + + expect(endpointsProperties).toBeDefined(); + expect(label).toEqual(endpointsConfig.configuration.name); + expect(serviceType).toEqual('objectstorage'); + expect(savePreferences).toEqual(true); + expect(disabled).toEqual(false); + expect(handleEndpointsSelection).toBeDefined(); + expect(defaultValue).toEqual(undefined); + expect(region).toEqual('us-east'); + expect(xFilter).toEqual({ region: 'us-east' }); + } +}); + it('test getFiltersForMetricsCallFromCustomSelect method', () => { const result = getMetricsCallCustomFilters( { diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts index 34b07701b8b..a875dec9481 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts @@ -14,6 +14,7 @@ import type { FilterValueType, } from '../Dashboard/CloudPulseDashboardLanding'; import type { CloudPulseCustomSelectProps } from '../shared/CloudPulseCustomSelect'; +import type { CloudPulseEndpointsSelectProps } from '../shared/CloudPulseEndpointsSelect'; import type { CloudPulseEndpoints } from '../shared/CloudPulseEndpointsSelect'; import type { CloudPulseNodeTypeFilterProps } from '../shared/CloudPulseNodeTypeFilter'; import type { CloudPulseRegionSelectProps } from '../shared/CloudPulseRegionSelect'; @@ -360,6 +361,48 @@ export const getTextFilterProperties = ( }; }; +/** + * This function helps in building the properties needed for endpoints selection component + * + * @param config - accepts a CloudPulseServiceTypeFilters of endpoints key + * @param handleEndpointsChange - the callback when we select new endpoints + * @param dashboard - the actual selected dashboard + * @param isServiceAnalyticsIntegration - only if this is false, we need to save preferences , else no need + * @returns CloudPulseEndpointsSelectProps + */ +export const getEndpointsProperties = ( + props: CloudPulseFilterProperties, + handleEndpointsChange: (endpoints: string[], savePref?: boolean) => void +): CloudPulseEndpointsSelectProps => { + const { filterKey, name: label, placeholder } = props.config.configuration; + const { + config, + dashboard, + dependentFilters, + isServiceAnalyticsIntegration, + preferences, + shouldDisable, + } = props; + return { + defaultValue: preferences?.[config.configuration.filterKey], + disabled: + shouldDisable || + shouldDisableFilterByFilterKey( + filterKey, + dependentFilters ?? {}, + dashboard, + preferences + ), + handleEndpointsSelection: handleEndpointsChange, + label, + placeholder, + serviceType: dashboard.service_type, + region: dependentFilters?.[REGION], + savePreferences: !isServiceAnalyticsIntegration, + xFilter: filterBasedOnConfig(config, dependentFilters ?? {}), + }; +}; + /** * This function helps in builder the xFilter needed to passed in a apiV4 call * @@ -666,11 +709,14 @@ export const filterUsingDependentFilters = ( if (Array.isArray(resourceValue) && Array.isArray(filterValue)) { return filterValue.some((val) => resourceValue.includes(String(val))); - } else if (Array.isArray(resourceValue)) { + } + if (Array.isArray(resourceValue)) { return resourceValue.includes(String(filterValue)); - } else { - return resourceValue === filterValue; } + if (Array.isArray(filterValue)) { + return (filterValue as string[]).includes(String(resourceValue)); + } + return resourceValue === filterValue; }); }); } diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts b/packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts index c93438a7e29..c152b66ab0d 100644 --- a/packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts +++ b/packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts @@ -1,8 +1,10 @@ import { capabilityServiceTypeMapping } from '@linode/api-v4'; import { + ENDPOINT, INTERFACE_IDS_PLACEHOLDER_TEXT, LINODE_REGION, + REGION, RESOURCE_ID, } from './constants'; import { CloudPulseAvailableViews, CloudPulseSelectTypes } from './models'; @@ -315,6 +317,55 @@ export const FIREWALL_CONFIG: Readonly = { serviceType: 'firewall', }; +export const OBJECTSTORAGE_CONFIG_BUCKET: Readonly = + { + capability: capabilityServiceTypeMapping['objectstorage'], + filters: [ + { + configuration: { + filterKey: REGION, + filterType: 'string', + isFilterable: true, + isMetricsFilter: true, + name: 'Region', + priority: 1, + neededInViews: [CloudPulseAvailableViews.central], + }, + name: 'Region', + }, + { + configuration: { + dependency: [REGION], + filterKey: ENDPOINT, + filterType: 'string', + isFilterable: false, + isMetricsFilter: false, + isMultiSelect: true, + name: 'Endpoints', + priority: 2, + neededInViews: [CloudPulseAvailableViews.central], + }, + name: 'Endpoints', + }, + { + configuration: { + dependency: [REGION, ENDPOINT], + filterKey: RESOURCE_ID, + filterType: 'string', + isFilterable: true, + isMetricsFilter: true, + isMultiSelect: true, + name: 'Buckets', + neededInViews: [CloudPulseAvailableViews.central], + placeholder: 'Select Buckets', + priority: 3, + }, + name: 'Buckets', + }, + ], + serviceType: 'objectstorage', + }; + export const FILTER_CONFIG: Readonly< Map > = new Map([ @@ -322,4 +373,5 @@ export const FILTER_CONFIG: Readonly< [2, LINODE_CONFIG], [3, NODEBALANCER_CONFIG], [4, FIREWALL_CONFIG], + [6, OBJECTSTORAGE_CONFIG_BUCKET], ]); diff --git a/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.test.ts b/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.test.ts index d3e0132dbfe..1f68344cf67 100644 --- a/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.test.ts +++ b/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.test.ts @@ -20,11 +20,14 @@ it('test getDashboardProperties method', () => { filterValue: { region: 'us-east' }, resource: 1, groupBy: [], + region: 'us-east', }); expect(result).toBeDefined(); expect(result.dashboardId).toEqual(mockDashboard.id); + expect(result.serviceType).toEqual(mockDashboard.service_type); expect(result.resources).toEqual(['1']); + expect(result.region).toEqual('us-east'); }); it('test checkMandatoryFiltersSelected method for time duration and resource', () => { @@ -93,6 +96,29 @@ it('test checkMandatoryFiltersSelected method for role', () => { expect(result).toBe(true); }); +it('checkMandatoryFiltersSelected method should return false if no region is selected for objectstorage service type', () => { + const result = checkMandatoryFiltersSelected({ + dashboardObj: { ...mockDashboard, service_type: 'objectstorage', id: 6 }, + filterValue: {}, + resource: 1, + timeDuration: { end: end.toISO(), preset, start: start.toISO() }, + groupBy: [], + }); + expect(result).toBe(false); +}); + +it('checkMandatoryFiltersSelected method should return true if region is selected for objectstorage service type', () => { + const result = checkMandatoryFiltersSelected({ + dashboardObj: { ...mockDashboard, service_type: 'objectstorage', id: 6 }, + filterValue: {}, + resource: 1, + timeDuration: { end: end.toISO(), preset, start: start.toISO() }, + groupBy: [], + region: 'ap-west', + }); + expect(result).toBe(true); +}); + it('test constructDimensionFilters method', () => { mockDashboard.service_type = 'dbaas'; const result = constructDimensionFilters({ diff --git a/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.ts b/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.ts index 7337e235b4d..edce614f656 100644 --- a/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.ts +++ b/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.ts @@ -23,10 +23,14 @@ interface ReusableDashboardFilterUtilProps { * The selected grouping criteria */ groupBy: string[]; + /** + * The selected region + */ + region?: string; /** * The selected resource id */ - resource: number; + resource: number | string; /** * The selected time duration */ @@ -40,7 +44,8 @@ interface ReusableDashboardFilterUtilProps { export const getDashboardProperties = ( props: ReusableDashboardFilterUtilProps ): DashboardProperties => { - const { dashboardObj, filterValue, resource, timeDuration, groupBy } = props; + const { dashboardObj, filterValue, resource, timeDuration, groupBy, region } = + props; return { additionalFilters: constructDimensionFilters({ dashboardObj, @@ -51,8 +56,10 @@ export const getDashboardProperties = ( dashboardId: dashboardObj.id, duration: timeDuration ?? defaultTimeDuration(), resources: [String(resource)], + serviceType: dashboardObj.service_type, savePref: false, groupBy, + region, }; }; @@ -63,7 +70,7 @@ export const getDashboardProperties = ( export const checkMandatoryFiltersSelected = ( props: ReusableDashboardFilterUtilProps ): boolean => { - const { dashboardObj, filterValue, resource, timeDuration } = props; + const { dashboardObj, filterValue, resource, timeDuration, region } = props; const serviceTypeConfig = FILTER_CONFIG.get(dashboardObj.id); if (!serviceTypeConfig) { @@ -74,6 +81,10 @@ export const checkMandatoryFiltersSelected = ( return false; } + if (dashboardObj.service_type === 'objectstorage' && !region) { + return false; + } + return serviceTypeConfig.filters.every(({ configuration }) => { const { filterKey, neededInViews } = configuration; diff --git a/packages/manager/src/features/CloudPulse/Utils/constants.ts b/packages/manager/src/features/CloudPulse/Utils/constants.ts index 786a4795805..64f98ddbbe4 100644 --- a/packages/manager/src/features/CloudPulse/Utils/constants.ts +++ b/packages/manager/src/features/CloudPulse/Utils/constants.ts @@ -8,6 +8,10 @@ export const SECONDARY_NODE = 'secondary'; export const REGION = 'region'; +export const ENTITY_REGION = 'entity_region'; + +export const ENDPOINT = 'endpoint'; + export const LINODE_REGION = 'associated_entity_region'; export const RESOURCES = 'resources'; @@ -85,6 +89,7 @@ export const NO_REGION_MESSAGE: Record = { linode: 'No Linodes configured in any regions.', nodebalancer: 'No NodeBalancers configured in any regions.', firewall: 'No firewalls configured in any Linode regions.', + objectstorage: 'No Object Storage buckets configured in any region.', }; export const HELPER_TEXT: Record = { diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx index 8ef969b8e97..c717a0b44d9 100644 --- a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx @@ -91,6 +91,11 @@ export interface CloudPulseWidgetProperties { */ linodeRegion?: string; + /** + * Selected region for the widget + */ + region?: string; + /** * List of resources available of selected service type */ @@ -149,7 +154,6 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { const { additionalFilters, - dashboardId, ariaLabel, authToken, availableMetrics, @@ -163,6 +167,8 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { unit, widget: widgetProp, linodeRegion, + dashboardId, + region, } = props; const timezone = @@ -262,6 +268,8 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { widget, groupBy: [...(widgetProp.group_by ?? []), ...groupBy], linodeRegion, + region, + serviceType, }), filters, // any additional dimension filters will be constructed and passed here }, diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx index e09fa2129e2..ee4d5c867b2 100644 --- a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidgetRenderer.tsx @@ -37,6 +37,10 @@ interface WidgetProps { manualRefreshTimeStamp?: number; metricDefinitions: ResourcePage | undefined; preferences?: AclpConfig; + /** + * Selected region for the widget + */ + region?: string; resourceList: CloudPulseResources[] | undefined; resources: string[]; savePref?: boolean; @@ -68,6 +72,7 @@ export const RenderWidgets = React.memo( savePref, groupBy, linodeRegion, + region, } = props; const getCloudPulseGraphProperties = ( @@ -176,6 +181,7 @@ export const RenderWidgets = React.memo( availableMetrics={availMetrics} isJweTokenFetching={isJweTokenFetching} linodeRegion={linodeRegion} + region={region} resources={resourceList!} savePref={savePref} /> diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx index 220b36f03bb..75ab29be939 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.tsx @@ -5,6 +5,7 @@ import NullComponent from 'src/components/NullComponent'; import { CloudPulseCustomSelect } from './CloudPulseCustomSelect'; import { CloudPulseDateTimeRangePicker } from './CloudPulseDateTimeRangePicker'; +import { CloudPulseEndpointsSelect } from './CloudPulseEndpointsSelect'; import { CloudPulseNodeTypeFilter } from './CloudPulseNodeTypeFilter'; import { CloudPulseRegionSelect } from './CloudPulseRegionSelect'; import { CloudPulseResourcesSelect } from './CloudPulseResourcesSelect'; @@ -13,6 +14,7 @@ import { CloudPulseTextFilter } from './CloudPulseTextFilter'; import type { CloudPulseCustomSelectProps } from './CloudPulseCustomSelect'; import type { CloudPulseDateTimeRangePickerProps } from './CloudPulseDateTimeRangePicker'; +import type { CloudPulseEndpointsSelectProps } from './CloudPulseEndpointsSelect'; import type { CloudPulseNodeTypeFilterProps } from './CloudPulseNodeTypeFilter'; import type { CloudPulseRegionSelectProps } from './CloudPulseRegionSelect'; import type { CloudPulseResourcesSelectProps } from './CloudPulseResourcesSelect'; @@ -24,6 +26,7 @@ export interface CloudPulseComponentRendererProps { componentProps: | CloudPulseCustomSelectProps | CloudPulseDateTimeRangePickerProps + | CloudPulseEndpointsSelectProps | CloudPulseNodeTypeFilterProps | CloudPulseRegionSelectProps | CloudPulseResourcesSelectProps @@ -37,6 +40,7 @@ const Components: { React.ComponentType< | CloudPulseCustomSelectProps | CloudPulseDateTimeRangePickerProps + | CloudPulseEndpointsSelectProps | CloudPulseNodeTypeFilterProps | CloudPulseRegionSelectProps | CloudPulseResourcesSelectProps @@ -54,6 +58,7 @@ const Components: { resource_id: CloudPulseResourcesSelect, tags: CloudPulseTagsSelect, associated_entity_region: CloudPulseRegionSelect, + endpoint: CloudPulseEndpointsSelect, }; const buildComponent = (props: CloudPulseComponentRendererProps) => { diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx index 7142c7ea322..404af3c14b1 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx @@ -10,6 +10,7 @@ import NullComponent from 'src/components/NullComponent'; import RenderComponent from '../shared/CloudPulseComponentRenderer'; import { DASHBOARD_ID, + ENDPOINT, INTERFACE_ID, LINODE_REGION, NODE_TYPE, @@ -21,6 +22,7 @@ import { } from '../Utils/constants'; import { getCustomSelectProperties, + getEndpointsProperties, getFilters, getNodeTypeProperties, getRegionProperties, @@ -235,6 +237,7 @@ export const CloudPulseDashboardFilterBuilder = React.memo( filterKey === REGION ? { [filterKey]: region, + [ENDPOINT]: undefined, [RESOURCES]: undefined, [TAGS]: undefined, } @@ -252,6 +255,16 @@ export const CloudPulseDashboardFilterBuilder = React.memo( [emitFilterChangeByFilterKey] ); + const handleEndpointsChange = React.useCallback( + (endpoints: string[], savePref: boolean = false) => { + emitFilterChangeByFilterKey(ENDPOINT, endpoints, endpoints, savePref, { + [ENDPOINT]: endpoints, + [RESOURCE_ID]: undefined, + }); + }, + [emitFilterChangeByFilterKey] + ); + const handleCustomSelectChange = React.useCallback( ( filterKey: string, @@ -357,6 +370,18 @@ export const CloudPulseDashboardFilterBuilder = React.memo( }, handleTextFilterChange ); + } else if (config.configuration.filterKey === ENDPOINT) { + return getEndpointsProperties( + { + config, + dashboard, + dependentFilters: dependentFilterReference.current, + isServiceAnalyticsIntegration, + preferences, + shouldDisable: isError || isLoading, + }, + handleEndpointsChange + ); } else { return getCustomSelectProperties( { @@ -380,6 +405,7 @@ export const CloudPulseDashboardFilterBuilder = React.memo( handleRegionChange, handleTextFilterChange, handleResourceChange, + handleEndpointsChange, handleCustomSelectChange, isServiceAnalyticsIntegration, preferences, diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index b051a64238c..e5aff744b51 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -1340,6 +1340,11 @@ export const handlers = [ region: 'us-mia', s3_endpoint: 'us-mia-1.linodeobjects.com', }), + objectStorageBucketFactoryGen2.build({ + endpoint_type: 'E3', + region: 'ap-west', + s3_endpoint: 'ap-west-1.linodeobjects.com', + }), ]; return HttpResponse.json(makeResourcePage(endpoints)); }), @@ -1448,6 +1453,18 @@ export const handlers = [ label: `obj-bucket-${randomBucketNumber}`, region, }); + if (region === 'ap-west') { + buckets.push( + objectStorageBucketFactoryGen2.build({ + cluster: `${region}-1`, + endpoint_type: 'E3', + s3_endpoint: 'ap-west-1.linodeobjects.com', + hostname: `obj-bucket-${randomBucketNumber}.${region}.linodeobjects.com`, + label: `obj-bucket-${randomBucketNumber}`, + region, + }) + ); + } return HttpResponse.json({ data: buckets.slice( @@ -3048,6 +3065,12 @@ export const handlers = [ regions: 'us-iad,us-east', alert: serviceAlertFactory.build({ scope: ['entity'] }), }), + serviceTypesFactory.build({ + label: 'Object Storage', + service_type: 'objectstorage', + regions: 'us-iad,us-east', + alert: serviceAlertFactory.build({ scope: ['entity'] }), + }), ], }; @@ -3136,6 +3159,16 @@ export const handlers = [ ); } + if (params.serviceType === 'objectstorage') { + response.data.push( + dashboardFactory.build({ + id: 6, + label: 'Object Storage Dashboard', + service_type: 'objectstorage', + }) + ); + } + return HttpResponse.json(response); }), http.get( @@ -3422,6 +3455,9 @@ export const handlers = [ } else if (id === '4') { serviceType = 'firewall'; dashboardLabel = 'Firewall Service I/O Statistics'; + } else if (id === '6') { + serviceType = 'objectstorage'; + dashboardLabel = 'Object Storage Service I/O Statistics'; } else { serviceType = 'linode'; dashboardLabel = 'Linode Service I/O Statistics'; @@ -3562,9 +3598,8 @@ export const handlers = [ }, { metric: { - entity_id: '789', + entity_id: 'obj-bucket-383.ap-west.linodeobjects.com', metric_name: 'average_cpu_usage', - node_id: 'primary-3', }, values: [ [1721854379, '0.3744841110560275'], diff --git a/packages/manager/src/queries/cloudpulse/queries.ts b/packages/manager/src/queries/cloudpulse/queries.ts index d53fedbfb23..1ac6118c044 100644 --- a/packages/manager/src/queries/cloudpulse/queries.ts +++ b/packages/manager/src/queries/cloudpulse/queries.ts @@ -16,6 +16,11 @@ import { } from '@linode/queries'; import { createQueryKeys } from '@lukemorales/query-key-factory'; +import { objectStorageQueries } from '../object-storage/queries'; +import { + getAllBucketsFromEndpoints, + getAllObjectStorageEndpoints, +} from '../object-storage/requests'; import { fetchCloudPulseMetrics } from './metrics'; import { getAllAlertsRequest, @@ -124,6 +129,14 @@ export const queryFactory = createQueryKeys(key, { case 'nodebalancer': return nodebalancerQueries.nodebalancers._ctx.all(params, filters); + case 'objectstorage': + return { + queryFn: () => getAllBuckets(), + queryKey: [ + ...objectStorageQueries.endpoints.queryKey, + objectStorageQueries.buckets.queryKey[1], + ], + }; case 'volumes': return volumeQueries.lists._ctx.all(params, filters); // in this we don't need to define our own query factory, we will reuse existing implementation in volumes.ts @@ -134,6 +147,23 @@ export const queryFactory = createQueryKeys(key, { token: (serviceType: string | undefined, request: JWETokenPayLoad) => ({ queryFn: () => getJWEToken(request, serviceType!), - queryKey: [serviceType, { resource_ids: request.entity_ids.sort() }], + queryKey: [serviceType, { resource_ids: request.entity_ids?.sort() }], }), }); + +const getAllBuckets = async () => { + const endpoints = await getAllObjectStorageEndpoints(); + + // Get all the buckets from the endpoints + const allBuckets = await getAllBucketsFromEndpoints(endpoints); + + // Throw the error if we encounter any error for any single call. + if (allBuckets.errors.length) { + throw new Error('Unable to fetch the data.'); + } + + // Filter the E0, E1 endpoint_type out and return the buckets + return allBuckets.buckets.filter( + (bucket) => bucket.endpoint_type !== 'E0' && bucket.endpoint_type !== 'E1' + ); +}; diff --git a/packages/manager/src/queries/cloudpulse/resources.ts b/packages/manager/src/queries/cloudpulse/resources.ts index b1f3dd89bfa..074c6c98eb9 100644 --- a/packages/manager/src/queries/cloudpulse/resources.ts +++ b/packages/manager/src/queries/cloudpulse/resources.ts @@ -14,6 +14,7 @@ export const useResourcesQuery = ( useQuery({ ...queryFactory.resources(resourceType, params, filters), enabled, + retry: resourceType === 'objectstorage' ? false : 3, select: (resources) => { if (!enabled) { return []; // Return empty array if the query is not enabled @@ -36,13 +37,18 @@ export const useResourcesQuery = ( } }); } + const id = + resourceType === 'objectstorage' + ? resource.hostname + : String(resource.id); return { engineType: resource.engine, - id: String(resource.id), - label: resource.label, + id, + label: resourceType === 'objectstorage' ? id : resource.label, region: resource.region, regions: resource.regions ? resource.regions : [], tags: resource.tags, + endpoint: resource.s3_endpoint, entities, clusterSize: resource.cluster_size, }; diff --git a/packages/utilities/src/__data__/regionsData.ts b/packages/utilities/src/__data__/regionsData.ts index 84587c53e5c..6b1f916e968 100644 --- a/packages/utilities/src/__data__/regionsData.ts +++ b/packages/utilities/src/__data__/regionsData.ts @@ -13,6 +13,7 @@ export const regions: Region[] = [ 'VPCs', 'Block Storage Migrations', 'Managed Databases', + 'Object Storage', ], country: 'in', id: 'ap-west', @@ -27,7 +28,7 @@ export const regions: Region[] = [ }, site_type: 'core', status: 'ok', - monitors: { alerts: ['Cloud Firewall'], metrics: [] }, + monitors: { alerts: ['Cloud Firewall'], metrics: ['Object Storage'] }, }, { capabilities: [