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 1156f31..55c6453 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 7ca8cfa..f58ddb0 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 00c92d4..7657659 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 c6a1f83..0b56818 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 358a182..8ea8ec1 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 7657659..1c19f23 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 8ea8ec1..01b26f7 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 94df575..c6cf855 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 993ee9a..702b7c6 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 1591bd8..bff2053 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 01b26f7..07bea28 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 a9efde6..263719c 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 0b56818..b0373b3 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 263719c..776f1cb 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 07bea28..3c26166 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(