Repository: linode/manager
Pull request #12174: upcoming: [DI-24898] - Add view control for cloudpulse filters
Author: login: ankita-akamai
Labels: name: Approved
description: Multiple approvals and ready to merge!
name: Cloud Pulse - Dashboards
Created: 2025-05-08T06:44:03Z | Merged: 2025-05-12T08:31:52Z

## Description 📝

Add view control for cloudpulse filters.

## Changes  🔄

- Added view control in filter configs at `filterConfig.ts` file. If a filter needs to be shown in a specific view - (Centralized or Service Page), that view should be added in the `neededInViews` prop.
- As part of the above change, new prop is introduced - `neededInViews` and the old prop - `neededInServicePage` is removed leading to updates in a few files.

## Target release date 🗓️
20 May 2025

## Preview 📷
No change in both, centralized and contextual view as we have just formed the base for upcoming features.
| Before | After |
| ------- | ------ |
| ![image](https://github.com/user-attachments/assets/bdf44238-c702-4b6a-8d4d-a8bbc8f13bfb) | ![image](https://github.com/user-attachments/assets/45084a84-9db5-43bc-ad50-b5b567da1c1f) |
| ![image](https://github.com/user-attachments/assets/5b3164b6-c8ed-4884-b8df-debca9e572a0) | ![image](https://github.com/user-attachments/assets/91e469dc-a722-4ba7-92fb-97fd78c86536) | 

**video:** 

Databases Centralized: [No Change]

https://github.com/user-attachments/assets/8c778a29-8d26-49dc-84a8-293c76da2e15

Databases Contextual: [No Change]

https://github.com/user-attachments/assets/3aa2c264-0782-42a7-abbf-b5846eaa7711

Linode Centralized: [No Change]

https://github.com/user-attachments/assets/b4691478-019c-4fda-a163-fcafe3663e67


## How to test 🧪

### Verification steps

- Navigate to metrics page.
- Visit `FilterConfig.ts` file, try adding or removing `central` or `service` view from `neededInViews` prop in filter configs and verify if doing this adds or removes those filters in the respective views in the metrics, for instance, centralized view or service_page view.
<details>
<summary> Author Checklists </summary>

## As an Author, to speed up the review process, I considered 🤔

👀 Doing a self review
❔ Our [contribution guidelines](https://github.com/linode/manager/blob/develop/docs/CONTRIBUTING.md)
🤏 Splitting feature into small PRs
➕ Adding a [changeset](https://github.com/linode/manager/blob/develop/docs/CONTRIBUTING.md#writing-a-changeset)
🧪 Providing/improving test coverage
 🔐 Removing all sensitive information from the code and PR description
🚩 Using a feature flag to protect the release
👣 Providing comprehensive reproduction steps
📑 Providing or updating our documentation
🕛 Scheduling a pair reviewing session
📱 Providing mobile support
♿  Providing accessibility support

<br/>

- [ ] I have read and considered all applicable items listed above.

## As an Author, before moving this PR from Draft to Open, I confirmed ✅

- [ ] All unit tests are passing
- [ ] TypeScript compilation succeeded without errors
- [ ] Code passes all linting rules

</details>

---


Diff:
diff --git a/packages/manager/.changeset/pr-12174-upcoming-features-1746687422617.md b/packages/manager/.changeset/pr-12174-upcoming-features-1746687422617.md
new file mode 100644
index 00000000000..27058892c4e
--- /dev/null
+++ b/packages/manager/.changeset/pr-12174-upcoming-features-1746687422617.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Upcoming Features
+---
+
+CloudPulse-Metrics: Upgrade filter configs at `filterConfig.ts` to have view based control of filters ([#12174](https://github.com/linode/manager/pull/12174))
diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts
index 98ab1652bf7..4e6e80ecc8c 100644
--- a/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts
+++ b/packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts
@@ -7,7 +7,7 @@ import {
   TAGS,
 } from './constants';
 import { FILTER_CONFIG } from './FilterConfig';
-import { CloudPulseSelectTypes } from './models';
+import { CloudPulseAvailableViews, CloudPulseSelectTypes } from './models';
 
 import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding';
 import type { CloudPulseCustomSelectProps } from '../shared/CloudPulseCustomSelect';
@@ -577,7 +577,11 @@ export const getFilters = (
 ): CloudPulseServiceTypeFilters[] | undefined => {
   return FILTER_CONFIG.get(dashboard.service_type)?.filters.filter((config) =>
     isServiceAnalyticsIntegration
-      ? config.configuration.neededInServicePage
-      : config.configuration.filterKey !== RELATIVE_TIME_DURATION
+      ? config.configuration.neededInViews.includes(
+          CloudPulseAvailableViews.service
+        )
+      : config.configuration.neededInViews.includes(
+          CloudPulseAvailableViews.central
+        )
   );
 };
diff --git a/packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts b/packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts
index 5a963dafad5..36ad75f86d7 100644
--- a/packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts
+++ b/packages/manager/src/features/CloudPulse/Utils/FilterConfig.ts
@@ -1,5 +1,5 @@
 import { RESOURCE_ID } from './constants';
-import { CloudPulseSelectTypes } from './models';
+import { CloudPulseAvailableViews, CloudPulseSelectTypes } from './models';
 
 import type { CloudPulseServiceTypeFilterMap } from './models';
 
@@ -17,8 +17,8 @@ export const LINODE_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
         isFilterable: false,
         isMetricsFilter: false,
         name: 'Region',
-        neededInServicePage: false,
         priority: 1,
+        neededInViews: [CloudPulseAvailableViews.central],
       },
       name: 'Region',
     },
@@ -32,7 +32,7 @@ export const LINODE_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
         isMultiSelect: true,
         isOptional: true,
         name: 'Tags',
-        neededInServicePage: false,
+        neededInViews: [CloudPulseAvailableViews.central],
         placeholder: 'Select Tags',
         priority: 4,
       },
@@ -47,7 +47,7 @@ export const LINODE_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
         isMetricsFilter: true,
         isMultiSelect: true,
         name: 'Resources',
-        neededInServicePage: false,
+        neededInViews: [CloudPulseAvailableViews.central],
         placeholder: 'Select Resources',
         priority: 2,
       },
@@ -61,7 +61,7 @@ export const LINODE_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
         isMetricsFilter: true,
         isMultiSelect: false,
         name: TIME_DURATION,
-        neededInServicePage: false,
+        neededInViews: [],
         placeholder: 'Select a Duration',
         priority: 3,
       },
@@ -82,7 +82,7 @@ export const DBAAS_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
         isMetricsFilter: false, // if it is false, it will go as a part of filter params, else global filter
         isMultiSelect: false,
         name: 'Database Engine',
-        neededInServicePage: false,
+        neededInViews: [CloudPulseAvailableViews.central],
         options: [
           {
             id: 'mysql',
@@ -107,8 +107,8 @@ export const DBAAS_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
         isFilterable: false,
         isMetricsFilter: false,
         name: 'Region',
-        neededInServicePage: false,
         priority: 1,
+        neededInViews: [CloudPulseAvailableViews.central],
       },
       name: 'Region',
     },
@@ -121,7 +121,7 @@ export const DBAAS_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
         isMetricsFilter: true,
         isMultiSelect: true,
         name: 'Database Clusters',
-        neededInServicePage: false,
+        neededInViews: [CloudPulseAvailableViews.central],
         placeholder: 'Select Database Clusters',
         priority: 3,
       },
@@ -135,7 +135,7 @@ export const DBAAS_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
         isMetricsFilter: true,
         isMultiSelect: false,
         name: TIME_DURATION,
-        neededInServicePage: false, // we will have a static time duration component, no need render from filter builder
+        neededInViews: [], // we will have a static time duration component, no need render from filter builder
         placeholder: 'Select a Duration',
         priority: 4,
       },
@@ -150,7 +150,10 @@ export const DBAAS_CONFIG: Readonly<CloudPulseServiceTypeFilterMap> = {
         isMetricsFilter: false, // if it is false, it will go as a part of filter params, else global filter
         isMultiSelect: false,
         name: 'Node Type',
-        neededInServicePage: true,
+        neededInViews: [
+          CloudPulseAvailableViews.service,
+          CloudPulseAvailableViews.central,
+        ],
         placeholder: 'Select a Node Type',
         priority: 5,
       },
diff --git a/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.ts b/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.ts
index ed0c12380da..a564bf2c24e 100644
--- a/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.ts
+++ b/packages/manager/src/features/CloudPulse/Utils/ReusableDashboardFilterUtils.ts
@@ -1,5 +1,6 @@
 import { defaultTimeDuration } from './CloudPulseDateTimePickerUtils';
 import { FILTER_CONFIG } from './FilterConfig';
+import { CloudPulseAvailableViews } from './models';
 
 import type { DashboardProperties } from '../Dashboard/CloudPulseDashboard';
 import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding';
@@ -68,10 +69,13 @@ export const checkMandatoryFiltersSelected = (
   }
 
   return serviceTypeConfig.filters.every(({ configuration }) => {
-    const { filterKey, neededInServicePage } = configuration;
+    const { filterKey, neededInViews } = configuration;
 
     // If the filter is not needed or optional, skip it
-    if (!neededInServicePage || configuration.isOptional) {
+    if (
+      !neededInViews.includes(CloudPulseAvailableViews.service) ||
+      configuration.isOptional
+    ) {
       return true;
     }
 
@@ -106,14 +110,14 @@ export const checkIfFilterNeededInMetricsCall = (
     const {
       filterKey: configFilterKey,
       isFilterable,
-      neededInServicePage,
+      neededInViews,
     } = configuration;
 
     return (
       // Indicates if this filter should be included in the metrics call
       configFilterKey === filterKey &&
       Boolean(isFilterable) &&
-      neededInServicePage
+      neededInViews.includes(CloudPulseAvailableViews.service)
     );
   });
 };
@@ -151,7 +155,7 @@ export const checkIfFilterBuilderNeeded = (dashboard?: Dashboard): boolean => {
     return false;
   }
 
-  return serviceTypeConfig.filters.some(
-    ({ configuration }) => configuration.neededInServicePage
+  return serviceTypeConfig.filters.some(({ configuration }) =>
+    configuration.neededInViews.includes(CloudPulseAvailableViews.service)
   );
 };
diff --git a/packages/manager/src/features/CloudPulse/Utils/models.ts b/packages/manager/src/features/CloudPulse/Utils/models.ts
index 979e1e00fcd..cf5120e16d5 100644
--- a/packages/manager/src/features/CloudPulse/Utils/models.ts
+++ b/packages/manager/src/features/CloudPulse/Utils/models.ts
@@ -134,9 +134,9 @@ export interface CloudPulseServiceTypeFiltersConfiguration {
    */
   name: string;
   /**
-   * This will be helpful, when we build a reusable component for integrating in service page, whether to show the filter there or not
+   *  This is helpful to determine the views in which the filter should be shown
    */
-  neededInServicePage: boolean;
+  neededInViews: CloudPulseAvailableViews[];
   /**
    * This is an optional field, needed if the select type is static, this is the list of options to be displayed in dropdown component
    */
@@ -185,3 +185,18 @@ export enum CloudPulseSelectTypes {
   // eslint-disable-next-line sonarjs/future-reserved-words
   static,
 }
+
+/**
+ * CloudPulseAvailableViews holds the views in which the filter should be shown
+ */
+export enum CloudPulseAvailableViews {
+  /**
+   * Represents the centralized Metrics view. If present in a filter's view configuration, that filter will be shown in the central page
+   */
+  central,
+
+  /**
+   * Represents the service page's Metrics view. If present in a filter's view configuration, that filter will be shown in the service page
+   */
+  service,
+}
diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.test.tsx
index 84e58e6623b..45e58792447 100644
--- a/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.test.tsx
+++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseComponentRenderer.test.tsx
@@ -12,6 +12,7 @@ import {
   getResourcesProperties,
   getTagsProperties,
 } from '../Utils/FilterBuilder';
+import { CloudPulseAvailableViews } from '../Utils/models';
 
 describe('ComponentRenderer component tests', () => {
   it('it should render provided tag filter in props', () => {
@@ -25,7 +26,7 @@ describe('ComponentRenderer component tests', () => {
         isMultiSelect: true,
         isOptional: true,
         name: 'Tags',
-        neededInServicePage: false,
+        neededInViews: [CloudPulseAvailableViews.central],
         placeholder: 'Select Tags',
         priority: 4,
       },
@@ -65,8 +66,8 @@ describe('ComponentRenderer component tests', () => {
         isFilterable: false,
         isMetricsFilter: false,
         name: 'Region',
-        neededInServicePage: false,
         priority: 1,
+        neededInViews: [CloudPulseAvailableViews.central],
       },
       name: 'Region',
     };
@@ -106,7 +107,7 @@ describe('ComponentRenderer component tests', () => {
         isMetricsFilter: true,
         isMultiSelect: true,
         name: 'Resources',
-        neededInServicePage: false,
+        neededInViews: [CloudPulseAvailableViews.central],
         placeholder: 'Select Resources',
         priority: 2,
       },
@@ -148,7 +149,10 @@ describe('ComponentRenderer component tests', () => {
         isMetricsFilter: false,
         isMultiSelect: false,
         name: 'Node Type',
-        neededInServicePage: true,
+        neededInViews: [
+          CloudPulseAvailableViews.service,
+          CloudPulseAvailableViews.central,
+        ],
         placeholder: 'Select a Node Type',
         priority: 5,
       },
diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx
index 6251b3dda39..e919ac3e035 100644
--- a/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx
+++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseDashboardFilterBuilder.tsx
@@ -12,7 +12,6 @@ import {
   DASHBOARD_ID,
   NODE_TYPE,
   REGION,
-  RELATIVE_TIME_DURATION,
   RESOURCE_ID,
   RESOURCES,
   TAGS,
@@ -26,9 +25,9 @@ import {
   getTagsProperties,
 } from '../Utils/FilterBuilder';
 import { FILTER_CONFIG } from '../Utils/FilterConfig';
+import { type CloudPulseServiceTypeFilters } from '../Utils/models';
 
 import type { FilterValueType } from '../Dashboard/CloudPulseDashboardLanding';
-import type { CloudPulseServiceTypeFilters } from '../Utils/models';
 import type { CloudPulseResources } from './CloudPulseResourcesSelect';
 import type { CloudPulseTags } from './CloudPulseTagsFilter';
 import type { AclpConfig, Dashboard } from '@linode/api-v4';
@@ -322,25 +321,18 @@ export const CloudPulseDashboardFilterBuilder = React.memo(
         );
       }
 
-      return filters
-        .filter(
-          (config) =>
-            isServiceAnalyticsIntegration
-              ? config.configuration.neededInServicePage
-              : config.configuration.filterKey !== RELATIVE_TIME_DURATION // time duration is always defined explicitly
-        )
-        .map((filter, index) => (
-          <Grid item key={filter.configuration.filterKey} md={4} sm={6} xs={12}>
-            {RenderComponent({
-              componentKey:
-                filter.configuration.type !== undefined
-                  ? 'customSelect'
-                  : filter.configuration.filterKey,
-              componentProps: { ...getProps(filter) },
-              key: index + filter.configuration.filterKey,
-            })}
-          </Grid>
-        ));
+      return filters.map((filter, index) => (
+        <Grid item key={filter.configuration.filterKey} md={4} sm={6} xs={12}>
+          {RenderComponent({
+            componentKey:
+              filter.configuration.type !== undefined
+                ? 'customSelect'
+                : filter.configuration.filterKey,
+            componentProps: { ...getProps(filter) },
+            key: index + filter.configuration.filterKey,
+          })}
+        </Grid>
+      ));
     }, [dashboard, getProps, isServiceAnalyticsIntegration]);
 
     if (


Reviews:
user: login: nikhagra-akamai
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T07:36:37Z
user: login: coliu-akamai
body: ✅ confirmed things look the same
✅ confirmed commenting out the views for specific filters hides that filter from the view

thank you!



state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:01:14Z
user: login: cpathipa
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:03:06Z
user: login: coliu-akamai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:05:48Z
user: login: ankita-akamai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:30:00Z
user: login: coliu-akamai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:41:25Z

Review comments:
diff_hunk: @@ -0,0 +1,5 @@
+---
+"@linode/manager": Upcoming Features
+---
+
+CloudPulse-Metrics: Upgraded filter configs at `filterConfig.ts` to have view based control of filters ([#12174](https://github.com/linode/manager/pull/12174))
path: packages/manager/.changeset/pr-12174-upcoming-features-1746687422617.md
user: login: coliu-akamai
body: (we use present tense - see https://linode.github.io/manager/CONTRIBUTING.html#writing-a-changeset)

```suggestion
CloudPulse-Metrics: Upgrade filter configs at `filterConfig.ts` to have view based control of filters ([#12174](https://github.com/linode/manager/pull/12174))
```
created_at: 2025-05-09T14:32:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,9 +134,9 @@ export interface CloudPulseServiceTypeFiltersConfiguration {
    */
   name: string;
   /**
-   * This will be helpful, when we build a reusable component for integrating in service page, whether to show the filter there or not
+   *  This is helpful to determine the views in which the filter should be shown
    */
-  neededInServicePage: boolean;
+  neededInViews: CloudPulseAvailableViews[];
path: packages/manager/src/features/CloudPulse/Utils/models.ts
user: login: coliu-akamai
body: nit: `neededInViews` reads a bit like it would be a boolean variable - maybe we can update the variable name to something like `inViewsFor`? whatever you think is best
created_at: 2025-05-09T14:35:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -185,3 +185,18 @@ export enum CloudPulseSelectTypes {
   // eslint-disable-next-line sonarjs/future-reserved-words
   static,
 }
+
+/**
+ * CloudPulseAvailableViews holds the views in which the filter should be shown
+ */
+export enum CloudPulseAvailableViews {
+  /**
+   * This is the centralized view, where the filter should be shown
+   */
path: packages/manager/src/features/CloudPulse/Utils/models.ts
user: login: coliu-akamai
body: should this be a bit more specific? eg
```suggestion
  /**
   * Represents the centralized Metrics view (at /metrics). If present in a filter's `neededInViews` configuration, that filter will be shown in the central view
   */
```
created_at: 2025-05-09T15:05:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,9 +134,9 @@ export interface CloudPulseServiceTypeFiltersConfiguration {
    */
   name: string;
   /**
-   * This will be helpful, when we build a reusable component for integrating in service page, whether to show the filter there or not
+   *  This is helpful to determine the views in which the filter should be shown
    */
-  neededInServicePage: boolean;
+  neededInViews: CloudPulseAvailableViews[];
path: packages/manager/src/features/CloudPulse/Utils/models.ts
user: login: ankita-akamai
body: I think `neededInViews` still makes sense in this context since it expresses where something is needed, not just whether it's needed. It's more about describing a set of views, so to me the name feels descriptive enough.
created_at: 2025-05-09T15:29:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,9 +134,9 @@ export interface CloudPulseServiceTypeFiltersConfiguration {
    */
   name: string;
   /**
-   * This will be helpful, when we build a reusable component for integrating in service page, whether to show the filter there or not
+   *  This is helpful to determine the views in which the filter should be shown
    */
-  neededInServicePage: boolean;
+  neededInViews: CloudPulseAvailableViews[];
path: packages/manager/src/features/CloudPulse/Utils/models.ts
user: login: coliu-akamai
body: sounds good!
created_at: 2025-05-09T15:41:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: linode-gh-bot
created_at: 2025-05-12T07:28:28Z
body: <!-- PULL REQUEST COMMENT: cloud-manager-ci-comment -->
<!-- BEGIN COMMENT SECTION: cloud-manager-ui-tests -->
## Cloud Manager UI test results
### :small_red_triangle: 1 failing test on [test run #7 ↗︎](https://baker.linode.com/job/manager/job/manager-test/job/PR-12174/7/display/redirect)

<table><thead><tr><td><strong>:x: Failing</strong></td><td><strong>:white_check_mark: Passing</strong></td><td><strong>:arrow_right_hook: Skipped</strong></td><td><strong>:clock1: Duration</strong></td></tr></thead><tbody><tr><td><code>1 Failing</code></td><td><code>585 Passing</code></td><td><code>6 Skipped</code></td><td><code>129m 36s</code></td></tr></tbody></table>


#### Details
<table><thead><tr><th colspan="3">Failing Tests</th></tr><tr><th></th><th>Spec</th><th>Test</th></tr></thead><tbody>
<tr><td>:x:</td><td><code>clone-linode.spec.ts</code></td><td><em>Cloud Manager Cypress Tests→clone linode » clone linode</em></td></tr>
</tbody></table>

#### Troubleshooting
Use this command to re-run the failing tests:

```bash
pnpm cy:run -s "cypress/e2e/core/linodes/clone-linode.spec.ts"
```
<!-- END COMMENT SECTION: cloud-manager-ui-tests -->
author_association: NONE
user: login: nikhagra-akamai
created_at: 2025-05-12T08:31:45Z
body: merging it, failing cypress is unrelated.
author_association: CONTRIBUTOR