Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions src/lib/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<id>`, 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[] = [
Expand All @@ -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)}`
})
},
Expand Down Expand Up @@ -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)
})
Expand All @@ -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<id>-<projectId>-…`. The palette indexes `d<id>`,
* 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]}`)
}
6 changes: 6 additions & 0 deletions src/types/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
33 changes: 33 additions & 0 deletions tests/search.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<id> 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<id>`,
// then `-<projectId>-<pod suffix>`.
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.
Expand Down
Loading