From 4f29bab58a6e52550d726ca08359fb2f675a2460 Mon Sep 17 00:00:00 2001 From: acoshift Date: Fri, 9 Oct 2026 16:44:58 +0700 Subject: [PATCH] Search deployments by id in the command palette. The palette matches a deployment's numeric id, its d resource name, and a pasted pod or service name that starts with that resource name. Prompter: ACS --- src/lib/search/index.ts | 34 ++++++++++++++++++++++++++++++---- src/types/api.d.ts | 6 ++++++ tests/fixtures/mocks.js | 2 ++ tests/search.spec.js | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/lib/search/index.ts b/src/lib/search/index.ts index 5b09e50a..a377d7a6 100644 --- a/src/lib/search/index.ts +++ b/src/lib/search/index.ts @@ -28,7 +28,21 @@ interface ResourceSource { group: string icon: string fn: string // api function name - map: (it: any, project: string) => { key: string, label: string, sublabel?: string, href: string } + map: (it: any, project: string) => { key: string, label: string, sublabel?: string, keywords?: string, href: string } +} + +/** + * Search text for a deployment's numeric id. The id is a string-encoded int64 + * (a JSON number would lose digits past 2^53). Also index `d`, the + * id-based Kubernetes resource name users see on pods and services. Hidden + * from the row, same as a project's numeric id. + */ +function deploymentIdKeywords (id: unknown): string | undefined { + const s = typeof id === 'string' + ? id.trim() + : typeof id === 'number' && Number.isSafeInteger(id) ? String(id) : '' + if (!/^\d+$/.test(s) || s === '0') return undefined + return `${s} d${s}` } const resourceSources: ResourceSource[] = [ @@ -40,6 +54,7 @@ const resourceSources: ResourceSource[] = [ key: `${it.location}/${it.name}`, label: it.name, sublabel: it.location, + keywords: deploymentIdKeywords(it.id), href: `/deployment/metrics?project=${p}&location=${enc(it.location)}&name=${enc(it.name)}` }) }, @@ -256,6 +271,7 @@ export async function fetchResourceEntries (project: string, fetch: typeof globa icon: src.icon, label: m.label, sublabel: m.sublabel, + keywords: m.keywords, href: m.href } as SearchEntry) }) @@ -267,14 +283,24 @@ export async function fetchResourceEntries (project: string, fetch: typeof globa } /** - * Token AND-match over label + sublabel + group, mirroring the project picker: - * every whitespace-separated token must appear somewhere in the haystack. + * Token AND-match over label + sublabel + keywords + group, mirroring the + * project picker: every whitespace-separated token must appear somewhere in + * the haystack. + * + * A pod or service name is `d--…`. The palette indexes `d`, + * so pasting the longer name still finds that deployment. */ export function filterEntries (entries: SearchEntry[], query: string): SearchEntry[] { const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean) if (!tokens.length) return entries return entries.filter((e) => { const haystack = `${e.label} ${e.sublabel ?? ''} ${e.keywords ?? ''} ${e.group}`.toLowerCase() - return tokens.every((t) => haystack.includes(t)) + return tokens.every((t) => tokenIn(haystack, t)) }) } + +function tokenIn (haystack: string, token: string): boolean { + if (haystack.includes(token)) return true + const m = /^d(\d+)-\d/.exec(token) + return !!m && haystack.includes(`d${m[1]}`) +} diff --git a/src/types/api.d.ts b/src/types/api.d.ts index d65677f0..951db551 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -399,6 +399,9 @@ declare namespace Api { project: string location: string name: string + // Numeric deployment id, string-encoded so it keeps the full int64. + // The id-based Kubernetes resource name is `d` + id. + id: string type: DeploymentType revision: number image: string @@ -457,6 +460,9 @@ declare namespace Api { project: string location: string name: string + // Numeric deployment id, string-encoded so it keeps the full int64. + // The id-based Kubernetes resource name is `d` + id. + id: string type: DeploymentType revision: number image: string diff --git a/tests/fixtures/mocks.js b/tests/fixtures/mocks.js index 41fdc937..80c30ab3 100644 --- a/tests/fixtures/mocks.js +++ b/tests/fixtures/mocks.js @@ -208,6 +208,8 @@ export const sampleDeployment = { project: 'test-project', location: 'gke', name: 'web', + // String-encoded int64 past Number.MAX_SAFE_INTEGER (2^53+1). + id: '9007199254740993', type: 'WebService', revision: 1, image: 'nginx:latest', diff --git a/tests/search.spec.js b/tests/search.spec.js index 8ad104a6..277588a6 100644 --- a/tests/search.spec.js +++ b/tests/search.spec.js @@ -44,6 +44,39 @@ test.describe('global search palette', () => { await expect(page.locator('.modal.is-active')).toHaveCount(0) }) + test('typing a deployment id, or its d resource name, opens that deployment', async ({ page }) => { + await setMocks({ + 'deployment.list': { ok: true, result: { items: [sampleDeployment] } }, + 'deployment.get': { ok: true, result: sampleDeployment } + }) + + await page.goto('/deployment?project=test-project') + await page.keyboard.press('/') + const dialog = page.locator('.modal.is-active') + const box = dialog.getByPlaceholder(/Search projects, deployments/) + + // The id is not shown on the row; matching it still surfaces the deployment. + await box.fill(sampleDeployment.id) + const webRow = dialog.getByRole('link', { name: /web/ }).first() + await expect(webRow).toBeVisible() + await expect(dialog.getByText('gke', { exact: false })).toBeVisible() + + // A different id matches nothing. + await box.fill('9007199254740994') + await expect(dialog.getByText(/No matches/)).toBeVisible() + + // Pod and service names use the id-based resource name `d`, + // then `--`. + await box.fill(`d${sampleDeployment.id}`) + await expect(webRow).toBeVisible() + await box.fill(`d${sampleDeployment.id}-486418960667672577-7f8c9d-abcde`) + await expect(webRow).toBeVisible() + + await webRow.click() + await expect(page).toHaveURL(/\/deployment\/metrics\?project=test-project&location=gke&name=web/) + await expect(page.locator('.modal.is-active')).toHaveCount(0) + }) + test('"/" is ignored while typing in a text field', async ({ page }) => { await page.goto('/deployment?project=test-project') // Inject a focused text field so the global "/" handler sees a typing target.