Repository: linode/manager
Pull request #11767: fix: [M3-9449] - Filter out Linodes in distributed regions from Create flow Backups & Clone tab
Author: login: hana-akamai
Labels: name: Bug
description: Fixes for regressions or bugs
name: Approved
description: Multiple approvals and ready to merge!
name: Linodes
description: Dealing with the Linodes section of the app
name: Gecko LA
description: Relating to Gecko LA
Created: 2025-02-28T20:46:18Z | Merged: 2025-03-04T19:44:53Z

## Description 📝
We can't clone or create backups from Linode instances in `distributed` regions so we need to filter them out from the `Backups` and `Clone Linode` tab in the Create flow

## Changes  🔄

- Filter out Linodes in `distributed` regions from the Create flow `Backups` and `Clone Linode` tab
- Surface invalid filter errors under the search input

## Target release date 🗓️
3/11

## Preview 📷

| Before  | After   |
| ------- | ------- |
| ![backups before](https://github.com/user-attachments/assets/12fb4a2a-272a-4e61-b5c5-136f99623112) | ![backups after](https://github.com/user-attachments/assets/15f344b7-a9b2-4bb9-bc40-208931d4dd2c) |
| ![clone before](https://github.com/user-attachments/assets/34237a6a-6ef0-4085-a183-3888808715e0) | ![clone after](https://github.com/user-attachments/assets/98c5e46d-bc1a-49d8-aa52-d2dec5864671) |

## How to test 🧪

### Prerequisites

(How to setup test environment)

- Have at least 1 Linode in a core region and 1 Linode in a distributed region

### Reproduction steps

(How to reproduce the issue, if applicable)

- [ ] On prod or a different branch, go to the Linode Create flow
- [ ] Go to the Backups tab (`/linodes/create?type=Backups`)
  - [ ] Notice Linodes in distributed regions are in the table
- [ ] Go to the Clone Linode tab (`/linodes/create?type=Clone+Linode`)
  - [ ] Notice Linodes in distributed regions are in the table

### Verification steps

(How to verify changes)

- [ ] Checkout this branch or PR preview link and go to the Linode Create flow
- [ ] Go to the Backups tab (`/linodes/create?type=Backups`)
  - [ ] Linodes in distributed regions should not be in the table
- [ ] Go to the Clone Linode tab (`/linodes/create?type=Clone+Linode`)
  - [ ] Linodes in distributed regions should not be in the table
- [ ] Filtering and ordering should still work as expected
- [ ] Invalid filter errors should be surfaced underneath the search input

```
pnpm test LinodeSelectTable.test
```

<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/>

- [x] 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
- [x] TypeScript compilation succeeded without errors
- [x] Code passes all linting rules

</details>


Diff:
diff --git a/packages/manager/.changeset/pr-11767-fixed-1740775625877.md b/packages/manager/.changeset/pr-11767-fixed-1740775625877.md
new file mode 100644
index 00000000000..46dab168c42
--- /dev/null
+++ b/packages/manager/.changeset/pr-11767-fixed-1740775625877.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Fixed
+---
+
+Filter out Linodes in distributed regions from Create flow Backups & Clone tab ([#11767](https://github.com/linode/manager/pull/11767))
diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.test.tsx
index c2feffafb1c..7755d89ffb5 100644
--- a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.test.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.test.tsx
@@ -9,11 +9,40 @@ import {
   renderWithThemeAndHookFormContext,
 } from 'src/utilities/testHelpers';
 
-import { LinodeSelectTable } from './LinodeSelectTable';
+import { LinodeSelectTable, getLinodeXFilter } from './LinodeSelectTable';
 
 beforeAll(() => mockMatchMedia());
 
 describe('Linode Select Table', () => {
+  it('should filter out Linodes in distributed regions', () => {
+    const { filter } = getLinodeXFilter(undefined, '');
+
+    expect(filter).toHaveProperty('site_type', 'core');
+  });
+
+  it('should search for label, id, ipv4, tags', () => {
+    const { filter } = getLinodeXFilter(undefined, '12345678');
+
+    expect(filter).toStrictEqual({
+      '+or': [
+        { label: { '+contains': '12345678' } },
+        { id: { '+contains': '12345678' } },
+        { ipv4: { '+contains': '12345678' } },
+        { tags: { '+contains': '12345678' } },
+      ],
+      site_type: 'core',
+    });
+  });
+
+  it('should return an error if the x-filter is invalid', () => {
+    const { filterError } = getLinodeXFilter(undefined, '123 456');
+
+    expect(filterError).toHaveProperty(
+      'message',
+      `Expected "!=", "<", "<=", "=", ">", ">=", [:~], or whitespace but "4" found.`
+    );
+  });
+
   it('should render Linodes from the API', async () => {
     const linodes = linodeFactory.buildList(10);
 
diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx
index 61bbfad27cb..0c7e7ec9811 100644
--- a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx
@@ -1,3 +1,4 @@
+import { getAPIFilterFromQuery } from '@linode/search';
 import { Box, Notice, Stack, Typography } from '@linode/ui';
 import Grid from '@mui/material/Grid2';
 import useMediaQuery from '@mui/material/useMediaQuery';
@@ -22,7 +23,6 @@ import { usePagination } from 'src/hooks/usePagination';
 import { useLinodesQuery } from 'src/queries/linodes/linodes';
 import { sendLinodePowerOffEvent } from 'src/utilities/analytics/customEventAnalytics';
 import { isPrivateIP } from 'src/utilities/ipUtils';
-import { isNumeric } from 'src/utilities/stringUtils';
 
 import {
   getGeneratedLinodeLabel,
@@ -34,6 +34,7 @@ import { SelectLinodeCard } from './SelectLinodeCard';
 import type { LinodeCreateFormValues } from '../utilities';
 import type { Linode } from '@linode/api-v4';
 import type { Theme } from '@mui/material';
+import type { UseOrder } from 'src/hooks/useOrder';
 
 interface Props {
   /**
@@ -79,17 +80,11 @@ export const LinodeSelectTable = (props: Props) => {
   const pagination = usePagination();
   const order = useOrder();
 
-  const filter = preselectedLinodeId
-    ? { id: preselectedLinodeId }
-    : {
-        '+or': [
-          { label: { '+contains': query } },
-          ...(isNumeric(query) ? [{ id: Number(query) }] : []), // let users filter by Linode id
-        ],
-        '+order': order.order,
-        '+order_by': order.orderBy,
-        // backups: { enabled: true }, womp womp! We can't filter on values within objects
-      };
+  const { filter, filterError } = getLinodeXFilter(
+    preselectedLinodeId,
+    query,
+    order
+  );
 
   const { data, error, isFetching, isLoading } = useLinodesQuery(
     {
@@ -146,6 +141,7 @@ export const LinodeSelectTable = (props: Props) => {
         }}
         clearable
         debounceTime={250}
+        errorText={filterError?.message}
         hideLabel
         isSearching={isFetching}
         label="Search"
@@ -240,3 +236,37 @@ export const LinodeSelectTable = (props: Props) => {
     </Stack>
   );
 };
+
+export const getLinodeXFilter = (
+  preselectedLinodeId: number | undefined,
+  query: string,
+  order?: UseOrder
+) => {
+  if (preselectedLinodeId) {
+    return {
+      id: preselectedLinodeId,
+    };
+  }
+
+  const { error: filterError, filter: apiFilter } = getAPIFilterFromQuery(
+    query,
+    {
+      searchableFieldsWithoutOperator: ['label', 'id', 'ipv4', 'tags'],
+    }
+  );
+
+  const filter = {
+    ...apiFilter,
+    site_type: 'core', // backups and cloning are not supported for distributed regions
+    // backups: { enabled: true }, womp womp! We can't filter on values within objects
+  };
+
+  if (order) {
+    return {
+      filter: { ...filter, '+order': order.order, '+order_by': order.orderBy },
+      filterError,
+    };
+  }
+
+  return { filter, filterError };
+};
diff --git a/packages/manager/src/hooks/useOrder.ts b/packages/manager/src/hooks/useOrder.ts
index f5e9d41abc8..ab28f230f3d 100644
--- a/packages/manager/src/hooks/useOrder.ts
+++ b/packages/manager/src/hooks/useOrder.ts
@@ -14,6 +14,10 @@ import type { BaseQueryParams } from 'src/utilities/queryParams';
 
 export type Order = 'asc' | 'desc';
 
+export interface UseOrder extends OrderSet {
+  handleOrderChange: (newOrderBy: string, newOrder: Order) => void;
+}
+
 /**
  * useOrder is a hook that allows you to handle ordering tables. It takes into account
  * the following items when determining initial order
@@ -31,7 +35,7 @@ export const useOrder = (
   initial?: OrderSet,
   preferenceKey?: string,
   prefix?: string
-) => {
+): UseOrder => {
   const { data: sortPreferences } = usePreferences(
     (preferences) => preferences?.sortKeys
   );


Reviews:
user: login: bnussman-akamai
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-28T20:55:47Z
user: login: hana-akamai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T19:06:18Z
user: login: hkhalil-akamai
body: Thanks @hana-akamai!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T20:29:59Z
user: login: bnussman-akamai
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T21:06:40Z
user: login: hana-akamai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T15:43:20Z
user: login: bnussman-akamai
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-04T17:55:36Z

Review comments:
diff_hunk: @@ -240,3 +233,20 @@ export const LinodeSelectTable = (props: Props) => {
     </Stack>
   );
 };
+
+export const getLinodeXFilter = (query: string, order?: UseOrder) => {
+  const filter = {
+    '+or': [
+      { label: { '+contains': query } },
+      ...(isNumeric(query) ? [{ id: Number(query) }] : []), // let users filter by Linode id
path: packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx
user: login: bnussman-akamai
body: Think we could use `getAPIFilterFromQuery` here while we're at it?
created_at: 2025-02-28T20:55:47Z
author_association: MEMBER
diff_hunk: @@ -240,3 +233,20 @@ export const LinodeSelectTable = (props: Props) => {
     </Stack>
   );
 };
+
+export const getLinodeXFilter = (query: string, order?: UseOrder) => {
+  const filter = {
+    '+or': [
+      { label: { '+contains': query } },
+      ...(isNumeric(query) ? [{ id: Number(query) }] : []), // let users filter by Linode id
path: packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx
user: login: hana-akamai
body: Done in https://github.com/linode/manager/pull/11767/commits/76c1a77acaef17fb7b536ccf9895c23762380c4d!
created_at: 2025-03-03T19:06:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -240,3 +233,21 @@ export const LinodeSelectTable = (props: Props) => {
     </Stack>
   );
 };
+
+export const getLinodeXFilter = (query: string, order?: UseOrder) => {
+  const { filter: apiFilter } = getAPIFilterFromQuery(query, {
+    searchableFieldsWithoutOperator: ['label', 'id'],
path: packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx
user: login: bnussman-akamai
body: We could probably add `ipv4` and `tags` here, which would allow users to search by tag or ipv4 too without prefixing with `ipv4: 0.0.0.0` or `tags: prod`
created_at: 2025-03-03T20:36:22Z
author_association: MEMBER
diff_hunk: @@ -240,3 +233,21 @@ export const LinodeSelectTable = (props: Props) => {
     </Stack>
   );
 };
+
+export const getLinodeXFilter = (query: string, order?: UseOrder) => {
+  const { filter: apiFilter } = getAPIFilterFromQuery(query, {
path: packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx
user: login: bnussman-akamai
body: We may want to handle errors somehow if a user types in an invalid search query. 

if the user types in invalid query, it doesn't perform any filtering, which may confuse the user
created_at: 2025-03-03T21:06:36Z
author_association: MEMBER
diff_hunk: @@ -240,3 +233,21 @@ export const LinodeSelectTable = (props: Props) => {
     </Stack>
   );
 };
+
+export const getLinodeXFilter = (query: string, order?: UseOrder) => {
+  const { filter: apiFilter } = getAPIFilterFromQuery(query, {
path: packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.tsx
user: login: hana-akamai
body: Opted to display it as error text under the input

<img src="https://github.com/user-attachments/assets/f829c8a6-a03b-4d16-b510-ddd69bae8d7d" width="500" />
created_at: 2025-03-04T15:43:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-03T20:31:35Z
body: <!-- add-pr-comment:add-pr-comment -->

**Coverage Report:** ✅<br>Base Coverage: 80.16%<br>Current Coverage: 80.18%

author_association: NONE
user: login: linode-gh-bot
created_at: 2025-03-04T19:41:48Z
body: <!-- PULL REQUEST COMMENT: cloud-manager-ci-comment -->
<!-- BEGIN COMMENT SECTION: cloud-manager-ui-tests -->
## Cloud Manager UI test results
### :tada: 534 passing tests on [test run #7 ↗︎](https://baker.linode.com/job/manager/job/manager-test/job/PR-11767/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>0 Failing</code></td><td><code>534 Passing</code></td><td><code>3 Skipped</code></td><td><code>110m 54s</code></td></tr></tbody></table>
<!-- END COMMENT SECTION: cloud-manager-ui-tests -->
author_association: NONE
user: login: cypress[bot]
created_at: 2025-03-04T20:42:01Z
body: 
<h3>
  <a href="https://cloud.cypress.io/projects/5rhsif?utm_source=github&utm_medium=flaky&utm_campaign=project">Cloud Manager E2E</a>&nbsp;
  <img src="https://assets.cypress.io/github-pr-comment-icons/chevron.svg" alt="" height="14" width="14" />&nbsp;
  <a href="https://cloud.cypress.io/projects/5rhsif/runs/7341/overview?utm_source=github&utm_medium=flaky&utm_campaign=run%20number">Run #7341</a>
</h3>

<details>
  <summary>
    <strong>Run Properties:</strong>&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="status check passed" height="10" width="10" />&nbsp;Passed&nbsp;<a href="https://cloud.cypress.io/projects/5rhsif/runs/7341/overview?utm_source=github&utm_medium=flaky&utm_campaign=run%20number">#7341</a></code> &nbsp;&bull;&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/linode/manager/commit/a5222228b77a3179bc19fc111b52bc1453e9609d">a5222228b7</a>: fix: [M3-9449] - Filter out Linodes in distributed regions from Create flow Back...</code>
  </summary><br />

  <table width="744">
    <tbody>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Project</strong>
        </td>
        <td width="520">
          <code><a href="https://cloud.cypress.io/projects/5rhsif?utm_source=github&utm_medium=flaky&utm_campaign=project">Cloud Manager E2E</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Branch Review</strong>
        </th>
        <td>
          <code><a href="https://cloud.cypress.io/projects/5rhsif/branches/develop/review?utm_source=github&utm_medium=flaky&utm_campaign=review%20changes&changeRequest=11767">develop</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run status</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="status check passed" height="10" width="10" />&nbsp;Passed&nbsp;<a href="https://cloud.cypress.io/projects/5rhsif/runs/7341/overview?utm_source=github&utm_medium=flaky&utm_campaign=run%20number">#7341</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run duration</strong></td>
        <td><code>30m 35s</code></td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Commit</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/linode/manager/commit/a5222228b77a3179bc19fc111b52bc1453e9609d">a5222228b7</a>: fix: [M3-9449] - Filter out Linodes in distributed regions from Create flow Back...</code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Committer</strong></td>
        <td><code>Hana Xu</code></td>
      </tr>
    </tbody>
    <tfoot>
      <tr />
      <tr height="48">
        <td colspan="2">
          <a href="https://cloud.cypress.io/projects/5rhsif/runs/7341/properties?utm_source=github&utm_medium=flaky&utm_campaign=properties">View all properties for this run ↗︎</a>
        </td>
      </tr>
    </tfoot>
  </table>
  <hr />
</details>

<table width="744">
  <thead>
    <tr />
    <tr height="48">
      <th colspan="2" align="left" width="744">Test results</th>
    </tr>
  </thead>
  <tbody>
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/failed.svg" alt="Tests that failed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5rhsif/runs/7341/overview?reviewViewBy=FAILED&utm_source=github&utm_medium=flaky&utm_campaign=failed%20tests">
          <strong>Failures</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/flaky-v3.svg" alt="Tests that were flaky" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5rhsif/runs/7341/overview?reviewViewBy=FLAKY&utm_source=github&utm_medium=flaky&utm_campaign=flaky%20tests">
          <strong>Flaky</strong>
        </a>
      </td>
      <td>
        <code><strong>5</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/pending.svg" alt="Tests that did not run due to a developer annotating a test with .skip" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5rhsif/runs/7341/test-results?utm_source=github&utm_medium=flaky&utm_campaign=pending%20tests&statuses=%5B%7B%22value%22%3A%22PENDING%22%2C%22label%22%3A%22PENDING%22%7D%5D">
          <strong>Pending</strong>
        </a>
      </td>
      <td>
        <code><strong>3</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/skipped-v3.svg" alt="Tests that did not run due to a failure in a mocha hook" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5rhsif/runs/7341/test-results?utm_source=github&utm_medium=flaky&utm_campaign=skipped%20tests&statuses=%5B%7B%22value%22%3A%22SKIPPED%22%2C%22label%22%3A%22SKIPPED%22%7D%5D">
          <strong>Skipped</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="Tests that passed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5rhsif/runs/7341/overview?utm_source=github&utm_medium=flaky&utm_campaign=passed%20tests">
          <strong>Passing</strong>
        </a>
      </td>
      <td>
        <code><strong>534</strong></code>
      </td>
    </tr><tfoot>
    <tr height="48"><td colspan="2"><a href="https://cloud.cypress.io/projects/5rhsif/branches/develop/review?utm_source=github&utm_medium=flaky&utm_campaign=review%20changes&changeRequest=11767">View all changes introduced in this branch ↗︎</a></td></tr></tfoot>
  </tbody>
</table>






author_association: NONE