From 845ef683aff4af5238a985c56416ea0dc5c421a9 Mon Sep 17 00:00:00 2001 From: Robert Luby Date: Mon, 15 Jun 2026 11:34:33 +0200 Subject: [PATCH 1/6] SRVOCF-840: Fix list page crashes and missing rows --- src/common/utils/utils.test.ts | 27 +++++++++- src/common/utils/utils.ts | 10 +++- src/pages/function-edit/FunctionEditPage.tsx | 4 +- .../function-list/FunctionsListPage.test.tsx | 50 +++++++++++++++++++ src/pages/function-list/FunctionsListPage.tsx | 20 +++++--- 5 files changed, 98 insertions(+), 13 deletions(-) diff --git a/src/common/utils/utils.test.ts b/src/common/utils/utils.test.ts index 1156f31f..55c64530 100644 --- a/src/common/utils/utils.test.ts +++ b/src/common/utils/utils.test.ts @@ -1,4 +1,4 @@ -import { getLanguageFromPath } from './utils'; +import { getLanguageFromPath, parseFuncYaml } from './utils'; describe('getLanguageFromPath', () => { it.each([ @@ -18,3 +18,28 @@ describe('getLanguageFromPath', () => { expect(getLanguageFromPath(path)).toBe(expected); }); }); + +describe('parseFuncYaml', () => { + it('parses name, namespace, and runtime', () => { + const yaml = 'name: my-function\nruntime: node\nnamespace: demo\n'; + expect(parseFuncYaml(yaml)).toEqual({ + name: 'my-function', + namespace: 'demo', + runtime: 'node', + }); + }); + + it('returns empty name when name field is missing', () => { + const yaml = 'runtime: go\nnamespace: demo\n'; + expect(parseFuncYaml(yaml)).toEqual({ + name: '', + namespace: 'demo', + runtime: 'go', + }); + }); + + it('throws when runtime field is missing', () => { + const yaml = 'name: my-func\nnamespace: demo\n'; + expect(() => parseFuncYaml(yaml)).toThrow('func.yaml missing runtime field'); + }); +}); diff --git a/src/common/utils/utils.ts b/src/common/utils/utils.ts index 7ca8cfac..f58ddb01 100644 --- a/src/common/utils/utils.ts +++ b/src/common/utils/utils.ts @@ -33,14 +33,20 @@ export function getLanguageFromPath(path: string): Language { return (extensionMap[ext] ?? 'plaintext') as Language; } -export function parseNamespaceAndRuntime(funcYaml: string): { +export function parseFuncYaml(funcYaml: string): { + name: string; namespace: string; runtime: string; } { + const nameMatch = funcYaml.match(/^name:\s*(.+)$/m); const runtimeMatch = funcYaml.match(/^runtime:\s*(.+)$/m); const namespaceMatch = funcYaml.match(/^namespace:\s*(.+)$/m); if (!runtimeMatch) throw new Error(`func.yaml missing runtime field`); - return { namespace: namespaceMatch?.[1]?.trim() ?? '', runtime: runtimeMatch[1].trim() }; + return { + name: nameMatch?.[1]?.trim() ?? '', + namespace: namespaceMatch?.[1]?.trim() ?? '', + runtime: runtimeMatch[1].trim(), + }; } export const handlerMap: Record = { diff --git a/src/pages/function-edit/FunctionEditPage.tsx b/src/pages/function-edit/FunctionEditPage.tsx index 00c92d47..7657659c 100644 --- a/src/pages/function-edit/FunctionEditPage.tsx +++ b/src/pages/function-edit/FunctionEditPage.tsx @@ -15,7 +15,7 @@ import { FileEntry, RepoMetadata } from '../../common/services/types'; import { getLanguageFromPath, handlerMap, - parseNamespaceAndRuntime, + parseFuncYaml, } from '../../common/utils/utils'; // --- page component --- @@ -203,7 +203,7 @@ function determineHandler(loadedFiles: FileEntry[]): string { const funcYaml = loadedFiles.find((f) => f.path === 'func.yaml'); if (!funcYaml) return ''; - const { runtime } = parseNamespaceAndRuntime(funcYaml.content); + const { runtime } = parseFuncYaml(funcYaml.content); const handlerPath = handlerMap[runtime]; if (loadedFiles.find((f) => f.path === handlerPath)) return handlerPath; diff --git a/src/pages/function-list/FunctionsListPage.test.tsx b/src/pages/function-list/FunctionsListPage.test.tsx index c6a1f830..0b56818a 100644 --- a/src/pages/function-list/FunctionsListPage.test.tsx +++ b/src/pages/function-list/FunctionsListPage.test.tsx @@ -520,6 +520,56 @@ describe('FunctionsListPage', () => { }); }); + it('skips repo when fetchFileContent throws (deleted repo)', async () => { + renderAuthenticated(); + mockUseSourceControl.mockReturnValue({ + listFunctionRepos: vi + .fn() + .mockResolvedValue([repoFixture('good-func'), repoFixture('deleted-repo')]), + fetchFileContent: vi.fn().mockImplementation((repo: { name: string }) => { + if (repo.name === 'deleted-repo') return Promise.reject(new Error('Not Found')); + return Promise.resolve(`name: ${repo.name}\nruntime: go\nnamespace: demo\n`); + }), + }); + mockUseClusterService.mockReturnValue(clusterData()); + + render( + + + , + ); + + const names = await screen.findAllByTestId('fn-name'); + expect(names).toHaveLength(1); + expect(names[0]).toHaveTextContent('good-func'); + }); + + it('uses func.yaml name instead of repo name for cluster matching', async () => { + renderAuthenticated(); + mockUseSourceControl.mockReturnValue({ + listFunctionRepos: vi.fn().mockResolvedValue([repoFixture('my-repo')]), + fetchFileContent: vi + .fn() + .mockResolvedValue('name: my-function\nruntime: node\nnamespace: demo\n'), + }); + mockUseClusterService.mockReturnValue( + clusterData({ + knativeServices: [ksvcFixture('my-function', 'True')], + deployments: [deploymentFixture('my-function', 1, 1)], + }), + ); + + render( + + + , + ); + + expect(await screen.findByTestId('fn-name')).toHaveTextContent('my-function'); + expect(screen.getByTestId('fn-status')).toHaveTextContent('Running'); + expect(mockUseClusterService).toHaveBeenLastCalledWith(['my-function']); + }); + it('removes a deleted repo from the list after refresh', async () => { renderAuthenticated(); const mockListRepos = vi diff --git a/src/pages/function-list/FunctionsListPage.tsx b/src/pages/function-list/FunctionsListPage.tsx index 358a1821..8ea8ec16 100644 --- a/src/pages/function-list/FunctionsListPage.tsx +++ b/src/pages/function-list/FunctionsListPage.tsx @@ -28,7 +28,7 @@ import { import { useClusterService } from '../../common/services/cluster/useClusterService'; import { SourceControlService } from '../../common/services/source-control/SourceControlService'; import { useSourceControlService } from '../../common/services/source-control/useSourceControlService'; -import { errorMessage, parseNamespaceAndRuntime } from '../../common/utils/utils'; +import { errorMessage, parseFuncYaml } from '../../common/utils/utils'; export default function FunctionsListPage() { return ( @@ -219,19 +219,23 @@ function useFunctionListPage(): { async function loadFunctionTableItems(svc: SourceControlService): Promise { const repos = await svc.listFunctionRepos(); - const items = await Promise.all( + const results = await Promise.all( repos.map(async (repo) => { - const funcYaml = await svc.fetchFileContent(repo, 'func.yaml'); - const { namespace, runtime } = parseNamespaceAndRuntime(funcYaml); - return newItem(repo.name, namespace, runtime); + try { + const funcYaml = await svc.fetchFileContent(repo, 'func.yaml'); + const { name, namespace, runtime } = parseFuncYaml(funcYaml); + return newItem(name || repo.name, namespace, runtime); + } catch { + return null; + } }), ); - return items; + return results.filter((item): item is FunctionTableItem => item !== null); } -function newItem(repoName: string, namespace: string, runtime: string): FunctionTableItem { +function newItem(name: string, namespace: string, runtime: string): FunctionTableItem { return { - name: repoName, + name, namespace, runtime, status: 'NotDeployed' as const, From 337bb6b11ea4532ecc1cd9a6a3a89369a769a7f1 Mon Sep 17 00:00:00 2001 From: Robert Luby Date: Mon, 15 Jun 2026 12:22:36 +0200 Subject: [PATCH 2/6] SRVOCF-840: fix lint import --- src/pages/function-edit/FunctionEditPage.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/function-edit/FunctionEditPage.tsx b/src/pages/function-edit/FunctionEditPage.tsx index 7657659c..1c19f239 100644 --- a/src/pages/function-edit/FunctionEditPage.tsx +++ b/src/pages/function-edit/FunctionEditPage.tsx @@ -12,11 +12,7 @@ import { ForgeConnectionProvider } from '../../common/context/ForgeConnectionPro import { SourceControlService } from '../../common/services/source-control/SourceControlService'; import { useSourceControlService } from '../../common/services/source-control/useSourceControlService'; import { FileEntry, RepoMetadata } from '../../common/services/types'; -import { - getLanguageFromPath, - handlerMap, - parseFuncYaml, -} from '../../common/utils/utils'; +import { getLanguageFromPath, handlerMap, parseFuncYaml } from '../../common/utils/utils'; // --- page component --- From 4cde369f62d3b23134187cd8ad479a16b923f99f Mon Sep 17 00:00:00 2001 From: Robert Luby Date: Tue, 16 Jun 2026 13:52:35 +0200 Subject: [PATCH 3/6] SRVOCF-840: fix review comments --- src/pages/function-list/FunctionsListPage.tsx | 13 +++++++--- .../components/FunctionTable.test.tsx | 24 +++++++++++++++++++ .../components/FunctionTable.tsx | 3 ++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/pages/function-list/FunctionsListPage.tsx b/src/pages/function-list/FunctionsListPage.tsx index 8ea8ec16..01b26f7b 100644 --- a/src/pages/function-list/FunctionsListPage.tsx +++ b/src/pages/function-list/FunctionsListPage.tsx @@ -224,8 +224,9 @@ async function loadFunctionTableItems(svc: SourceControlService): Promise item !== null); } -function newItem(name: string, namespace: string, runtime: string): FunctionTableItem { +function newItem( + name: string, + repoName: string, + namespace: string, + runtime: string, +): FunctionTableItem { return { name, + repoName, namespace, runtime, status: 'NotDeployed' as const, diff --git a/src/pages/function-list/components/FunctionTable.test.tsx b/src/pages/function-list/components/FunctionTable.test.tsx index 94df5751..c6cf855f 100644 --- a/src/pages/function-list/components/FunctionTable.test.tsx +++ b/src/pages/function-list/components/FunctionTable.test.tsx @@ -37,6 +37,7 @@ const mockDeployment = { const mockFunctions: FunctionTableItem[] = [ { name: 'my-func', + repoName: 'my-func', runtime: 'go', status: 'Running', url: 'http://my-func.demo.svc', @@ -46,6 +47,7 @@ const mockFunctions: FunctionTableItem[] = [ }, { name: 'idle-func', + repoName: 'idle-func', runtime: 'node', status: 'NotDeployed', replicas: 0, @@ -127,6 +129,28 @@ describe('FunctionTable', () => { expect(onEdit).toHaveBeenCalledWith('my-func'); }); + it('calls onEdit with repoName, not display name', async () => { + const onEdit = vi.fn(); + const user = userEvent.setup(); + const fn: FunctionTableItem = { + name: 'my-function', + repoName: 'my-repo', + runtime: 'node', + status: 'Running', + replicas: 1, + namespace: 'demo', + }; + + render( + + + , + ); + + await user.click(screen.getByRole('button', { name: 'Edit' })); + expect(onEdit).toHaveBeenCalledWith('my-repo'); + }); + it('launches delete modal when delete button is clicked', async () => { const mockLauncher = vi.fn(); mockUseDeleteModal.mockReturnValue(mockLauncher); diff --git a/src/pages/function-list/components/FunctionTable.tsx b/src/pages/function-list/components/FunctionTable.tsx index 993ee9a0..702b7c68 100644 --- a/src/pages/function-list/components/FunctionTable.tsx +++ b/src/pages/function-list/components/FunctionTable.tsx @@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next'; export interface FunctionTableItem { name: string; + repoName: string; runtime: string; status: FunctionStatus; url?: string; @@ -83,7 +84,7 @@ export function FunctionTable({ variant="plain" aria-label={t('Edit')} icon={} - onClick={() => onEdit(fn.name)} + onClick={() => onEdit(fn.repoName)} /> From eb2573b783bbd7b7356d23a98c9bbcd4240b54e5 Mon Sep 17 00:00:00 2001 From: Robert Luby Date: Fri, 19 Jun 2026 09:55:18 +0200 Subject: [PATCH 4/6] fix: show error items instead of dropping repos When fetchFileContent fails for a repo, return an error table item instead of null. This surfaces the failure to the user rather than silently hiding the repo from the list. Co-Authored-By: Claude --- docs/claude-progress.txt | 9 +++++++++ src/pages/function-list/FunctionsListPage.tsx | 7 ++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/claude-progress.txt b/docs/claude-progress.txt index 1591bd88..bff2053a 100644 --- a/docs/claude-progress.txt +++ b/docs/claude-progress.txt @@ -1,6 +1,15 @@ # Claude Progress Log # Newest entries first. Agents: append your entry at the top after the header. +--- +## 2026-06-19 | Session: Error handling in list page +Worked on: Improve error handling for failed func.yaml fetches +Completed: +- Changed catch block in loadFunctionTableItems to return an error item instead of silently dropping the repo (was returning null, now returns item with status 'Error') +- Removed unused error parameter from catch block +Left off: Change is staged and ready for commit on SRVOCF-840 branch. +Blockers: None + --- ## 2026-05-21 | Session: Page co-location restructure (continued) Worked on: Follow-up fixes from review, test cleanup, docs diff --git a/src/pages/function-list/FunctionsListPage.tsx b/src/pages/function-list/FunctionsListPage.tsx index 01b26f7b..07bea287 100644 --- a/src/pages/function-list/FunctionsListPage.tsx +++ b/src/pages/function-list/FunctionsListPage.tsx @@ -225,9 +225,10 @@ async function loadFunctionTableItems(svc: SourceControlService): Promise', ''); + item.status = 'Error'; + return item; } }), ); From 9c0ff8eb92c04c2f4d4c432bd6335260593ec37a Mon Sep 17 00:00:00 2001 From: Robert Luby Date: Fri, 19 Jun 2026 10:00:56 +0200 Subject: [PATCH 5/6] test: update test for error item behavior The list page now shows failed repos as error items instead of dropping them. Update the test to expect both items in the table and verify the failed repo appears by name. Co-Authored-By: Claude --- docs/claude-progress.txt | 6 +++++- src/pages/function-list/FunctionsListPage.test.tsx | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/claude-progress.txt b/docs/claude-progress.txt index a9efde68..263719cc 100644 --- a/docs/claude-progress.txt +++ b/docs/claude-progress.txt @@ -7,7 +7,11 @@ Worked on: Improve error handling for failed func.yaml fetches Completed: - Changed catch block in loadFunctionTableItems to return an error item instead of silently dropping the repo (was returning null, now returns item with status 'Error') - Removed unused error parameter from catch block -Left off: Change is staged and ready for commit on SRVOCF-840 branch. +- Updated test: 'skips repo' renamed to 'shows error item', now expects both items in the table (good repo + error repo) instead of only the good one +Left off: Both changes committed on SRVOCF-840 branch. +Blockers: None + +--- ## 2026-06-16 | Session: PR #36 - GitHub Pages local dev + clipboard fix (SRVOCF-843) Worked on: Local GitHub Pages dev support, clipboard copy error handling, PR review feedback Completed: diff --git a/src/pages/function-list/FunctionsListPage.test.tsx b/src/pages/function-list/FunctionsListPage.test.tsx index 0b56818a..b0373b30 100644 --- a/src/pages/function-list/FunctionsListPage.test.tsx +++ b/src/pages/function-list/FunctionsListPage.test.tsx @@ -520,7 +520,7 @@ describe('FunctionsListPage', () => { }); }); - it('skips repo when fetchFileContent throws (deleted repo)', async () => { + it('shows error item when fetchFileContent throws (deleted repo)', async () => { renderAuthenticated(); mockUseSourceControl.mockReturnValue({ listFunctionRepos: vi @@ -540,8 +540,9 @@ describe('FunctionsListPage', () => { ); const names = await screen.findAllByTestId('fn-name'); - expect(names).toHaveLength(1); + expect(names).toHaveLength(2); expect(names[0]).toHaveTextContent('good-func'); + expect(names[1]).toHaveTextContent('deleted-repo'); }); it('uses func.yaml name instead of repo name for cluster matching', async () => { From ce1db3b945fdd3da845bf7b742d97202107375fe Mon Sep 17 00:00:00 2001 From: Robert Luby Date: Fri, 19 Jun 2026 14:09:55 +0200 Subject: [PATCH 6/6] fix: address PR review on error handling Replace '' with empty strings so TextOrDash renders em dashes automatically. Add console.error for developer debugging. Remove the null filter since the catch block always returns an item now. Co-Authored-By: Claude --- docs/claude-progress.txt | 12 ++++++------ src/pages/function-list/FunctionsListPage.tsx | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/claude-progress.txt b/docs/claude-progress.txt index 263719cc..776f1cba 100644 --- a/docs/claude-progress.txt +++ b/docs/claude-progress.txt @@ -2,13 +2,13 @@ # Newest entries first. Agents: append your entry at the top after the header. --- -## 2026-06-19 | Session: Error handling in list page -Worked on: Improve error handling for failed func.yaml fetches +## 2026-06-19 | Session: SRVOCF-840 PR review fixes +Worked on: Address PR review comments on error handling catch block Completed: -- Changed catch block in loadFunctionTableItems to return an error item instead of silently dropping the repo (was returning null, now returns item with status 'Error') -- Removed unused error parameter from catch block -- Updated test: 'skips repo' renamed to 'shows error item', now expects both items in the table (good repo + error repo) instead of only the good one -Left off: Both changes committed on SRVOCF-840 branch. +- Replaced '' with empty strings for namespace/runtime (TextOrDash renders em dashes automatically) +- Added console.error logging in catch block for developer debugging +- Removed redundant null filter since catch always returns an item now +Left off: Changes committed on SRVOCF-840 branch. Blockers: None --- diff --git a/src/pages/function-list/FunctionsListPage.tsx b/src/pages/function-list/FunctionsListPage.tsx index 07bea287..3c261668 100644 --- a/src/pages/function-list/FunctionsListPage.tsx +++ b/src/pages/function-list/FunctionsListPage.tsx @@ -225,14 +225,15 @@ async function loadFunctionTableItems(svc: SourceControlService): Promise', ''); + } catch (err) { + console.error(`Failed to load func.yaml for ${repo.name}:`, err); + const item = newItem(repo.name, repo.name, '', ''); item.status = 'Error'; return item; } }), ); - return results.filter((item): item is FunctionTableItem => item !== null); + return results; } function newItem(