diff --git a/packages/manager/.changeset/pr-11112-added-1729086990373.md b/packages/manager/.changeset/pr-11112-added-1729086990373.md new file mode 100644 index 00000000000..12a00fcc645 --- /dev/null +++ b/packages/manager/.changeset/pr-11112-added-1729086990373.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Add the capability to search for a Linode by ID using the main search tool ([#11112](https://github.com/linode/manager/pull/11112)) diff --git a/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts b/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts new file mode 100644 index 00000000000..dcc0b7c133a --- /dev/null +++ b/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts @@ -0,0 +1,44 @@ +import { ui } from 'support/ui'; +import { cleanUp } from 'support/util/cleanup'; +import { authenticate } from 'support/api/authentication'; +import { createTestLinode } from 'support/util/linodes'; +import type { Linode } from '@linode/api-v4'; + +authenticate(); +describe('Search Linodes', () => { + beforeEach(() => { + cleanUp(['linodes']); + cy.tag('method:e2e'); + }); + + /* + * - Confirm that linodes are searchable and filtered in the UI. + */ + it('create a linode and make sure it shows up in the table and is searchable in main search tool', () => { + cy.defer(() => createTestLinode({ booted: true })).then( + (linode: Linode) => { + cy.visitWithLogin('/linodes'); + cy.get(`[data-qa-linode="${linode.label}"]`) + .should('be.visible') + .within(() => { + cy.contains('Running').should('be.visible'); + }); + + // Confirm that linode is listed on the landing page. + cy.findByText(linode.label).should('be.visible'); + + // Use the main search bar to search and filter linode by label + cy.get('[id="main-search"').type(linode.label); + ui.autocompletePopper.findByTitle(linode.label).should('be.visible'); + + // Use the main search bar to search and filter linode by id value + cy.get('[id="main-search"').clear().type(`${linode.id}`); + ui.autocompletePopper.findByTitle(linode.label).should('be.visible'); + + // Use the main search bar to search and filter linode by id: pattern + cy.get('[id="main-search"').clear().type(`id:${linode.id}`); + ui.autocompletePopper.findByTitle(linode.label).should('be.visible'); + } + ); + }); +}); diff --git a/packages/manager/src/features/Search/refinedSearch.ts b/packages/manager/src/features/Search/refinedSearch.ts index 2624d2fce4c..d844566d379 100644 --- a/packages/manager/src/features/Search/refinedSearch.ts +++ b/packages/manager/src/features/Search/refinedSearch.ts @@ -5,7 +5,7 @@ import searchString from 'search-string'; import type { SearchField, SearchableItem } from './search.interfaces'; export const COMPRESSED_IPV6_REGEX = /^([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,7})?::([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,7})?$/; -const DEFAULT_SEARCH_FIELDS = ['label', 'tags', 'ips']; +const DEFAULT_SEARCH_FIELDS = ['label', 'tags', 'ips', 'value']; // ============================================================================= // REFINED SEARCH @@ -166,6 +166,11 @@ export const doesSearchTermMatchItemField = ( const fieldValue = ensureValueIsString(flattenedItem[field]); + // Handle numeric comparison (e.g., for the "value" field to search linode by id) + if (typeof fieldValue === 'number') { + return fieldValue === Number(query); // Ensure exact match for numeric fields + } + if (caseSensitive) { return fieldValue.includes(query); } else { @@ -177,6 +182,7 @@ export const doesSearchTermMatchItemField = ( export const flattenSearchableItem = (item: SearchableItem) => ({ label: item.label, type: item.entityType, + value: item.value, ...item.data, }); @@ -203,7 +209,7 @@ export const getQueryInfo = (parsedQuery: any) => { }; }; -// Our entities have several fields we'd like to search: "tags", "label", "ips". +// Our entities have several fields we'd like to search: "tags", "label", "ips", "value". // A user might submit the query "tag:my-app". In this case, we want to trade // "tag" for "tags", since "tags" is the actual name of the intended property. export const getRealEntityKey = (key: string): SearchField | string => { @@ -211,9 +217,11 @@ export const getRealEntityKey = (key: string): SearchField | string => { const LABEL: SearchField = 'label'; const IPS: SearchField = 'ips'; const TYPE: SearchField = 'type'; + const VALUE: SearchField = 'value'; const substitutions = { group: TAGS, + id: VALUE, ip: IPS, is: TYPE, name: LABEL, diff --git a/packages/manager/src/features/Search/search.interfaces.ts b/packages/manager/src/features/Search/search.interfaces.ts index e8c45d5c334..a5d035f5f55 100644 --- a/packages/manager/src/features/Search/search.interfaces.ts +++ b/packages/manager/src/features/Search/search.interfaces.ts @@ -22,7 +22,7 @@ export type SearchableEntityType = | 'volume'; // These are the properties on our entities we'd like to search -export type SearchField = 'ips' | 'label' | 'tags' | 'type'; +export type SearchField = 'ips' | 'label' | 'tags' | 'type' | 'value'; export interface SearchResultsByEntity { buckets: SearchableItem[]; diff --git a/packages/manager/src/store/selectors/getSearchEntities.ts b/packages/manager/src/store/selectors/getSearchEntities.ts index f7cbef079f3..d7e9bf8d653 100644 --- a/packages/manager/src/store/selectors/getSearchEntities.ts +++ b/packages/manager/src/store/selectors/getSearchEntities.ts @@ -158,7 +158,7 @@ export const bucketToSearchableItem = ( cluster: bucket.cluster, created: bucket.created, description: readableBytes(bucket.size).formatted, - icon: 'bucket', + icon: 'storage', label: bucket.label, path: `/object-storage/buckets/${bucket.cluster}/${bucket.label}`, },