Skip to content
This repository was archived by the owner on Jun 26, 2026. It is now read-only.

Commit e1cb3f2

Browse files
twoGiantsclaude
andcommitted
feat: add Function Edit Page
File tree browser and code editor for viewing and editing function source code on GitHub. Files load from the Git Data API, the handler file is auto-selected based on func.yaml runtime. Save & Deploy pushes changes back via updateRepo, which caches the last commit SHA to handle GitHub's eventually consistent ref storage. Migrate test runner from Jest to Vitest with MSW 2.x for GitHub API mocking. Merge RepoInfo and SourceRepo into single RepoMetadata type. Extract shared utils (parseNamespaceAndRuntime, getLanguageFromPath, handlerMap). Add CSP connect-src for GitHub API in dev mode. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Stanislav Jakuschevskij <sjakusch@redhat.com>
1 parent 0d93997 commit e1cb3f2

42 files changed

Lines changed: 3292 additions & 2472 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ This is the project map. Read this first, every session.
77
FaaS PoC UI for OpenShift Console — React + TypeScript + Webpack + PatternFly 6 + OCP Dynamic Plugin SDK.
88
See `docs/design/` for full design specs.
99

10+
## Communication
11+
12+
- Ask before staring modifications of code.
13+
- Ask before starting a new task or making a design decision.
14+
- Once actively implementing, keep going without asking. Only stop to ask when blocked by sandbox, permissions, or ambiguous requirements.
15+
1016
## Writing Style
1117

1218
No em dashes (``). Use commas, periods, or parentheses instead.

docs/ARCHITECTURE.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,23 @@ Arrows mean "imports / depends on."
3939
- Services never import Components or Views
4040
- No circular dependencies
4141

42-
## Page / Component / Hook Rules
42+
## React
4343

44-
**Components are simple** — they receive data via props, render it, and call callbacks to return data to the parent (or use context). No logic at the top of a component.
44+
### Page / Component / Hook Rules
4545

46-
**Pages are smart** — they use central hooks (e.g. `useClusterService`, `useSourceControl`) to fetch, prepare, and transform all data needed for downstream components.
46+
**Components are simple by default** — they receive data via props, render it, and call callbacks. No logic at the top of a component.
4747

48-
**Extract logic into hooks** — if a page or component has any logic (state management, data transformation, side effects), extract it into a co-named hook: `FunctionTable.tsx``useFunctionTable.ts`, `FunctionsListPage.tsx``useFunctionsListPage.ts`. If there is no logic, no hook is needed.
48+
**A component may own its own data and state when it encapsulates a self-contained capability that is not specific to any one page** (e.g., forge connection, auth flows, notification subscriptions). The component becomes the single owner of that concern. Pages consume it without orchestrating its internals.
49+
50+
**Pages are smart for page-specific data** — they use central hooks (e.g. `useClusterService`, `useSourceControl`) to fetch, prepare, and transform all data needed for downstream components.
51+
52+
**Extract logic into hooks** — if a page or component has any logic (state management, data transformation, side effects), extract it into a custom hook. If the hook is reused by multiple components, put it in a separate file: `useFunctionTable.ts`. If the hook is only used by one component, keep it in the same file, do not export it. If there is no logic, no hook is needed.
53+
54+
**File ordering** — within a file, put the exported component at the top, then its hook below, then helper functions at the bottom. Readers see the main thing first and can drill down.
55+
56+
### Performance
57+
58+
- **No speculative memoization**: Do not wrap every function in `useCallback` or every value in `useMemo` as a habit. Use them when there is a concrete reason: a `React.memo` child that depends on a stable reference, or a known re-render path (e.g., a sibling component re-rendering on every keystroke). Plain functions and derived values are the default.
4959

5060
## Architectural Guidance
5161

docs/STYLEGUIDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@
1717
- **No `any` type**: Use proper TypeScript types.
1818
- **No `console.log`**: Use structured approach if logging needed.
1919
- **Naming**: `use*` for hooks, `*Service` for services, PascalCase for components/types.
20+
- **Co-locate helper functions**: Keep helper functions in the same file as the component or hook that uses them. Test them indirectly through the consumer's tests. Only extract to `utils/` when shared across multiple unrelated modules.
2021

2122
## Documentation
2223

2324
- **No em dashes (``)**. Use commas, periods, or parentheses instead.
2425

26+
## CSS
27+
28+
- **PatternFly first**: Use PatternFly component props for all layout and spacing. Only fall back to custom CSS when PatternFly does not cover the need.
29+
- **Relative units only**: When custom CSS is necessary, use `rem` and `em` for all sizing. Never use `px`.
30+
2531
## OCP Plugin Styling Constraints
2632

2733
The `.stylelintrc.yaml` enforces strict rules to prevent breaking the OpenShift Console:

docs/WORKFLOW.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Every session, before doing any work:
1717
After [Startup Sequence](#startup-sequence), work through the picked feature:
1818

1919
1. **Plan** — read `docs/ARCHITECTURE.md` + `docs/STYLEGUIDE.md` + `docs/TESTING.md`, then use `/brainstorming` to design the chosen feature from `docs/features.json`, then use `/writing-plans` to create implementation plan → `docs/plans/active/<NNN>-<type>-<short-name>.md`
20-
2. **Branch** — create feature branch per [Branching](#branching) convention
20+
2. **Branch** — create feature branch per [Branching](#branching) convention. Immediately push and open a **draft PR** (`gh pr create --draft`) to reserve the PR number for other contributors' branch numbering.
2121
3. **Implement** — using `/executing-plans` skill
2222
4. **Review** — code review using `/requesting-code-review` skill, fix found issues
2323
5. **Manual Test** — use browser automation and validate it works in the browser

docs/claude-progress.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
# Claude Progress Log
22
# Newest entries first. Agents: append your entry at the top after the header.
33

4+
---
5+
## 2026-04-28 | Session: Function Edit Page
6+
Worked on: Full Function Edit Page feature (plan 016)
7+
Completed:
8+
- Function Edit Page with FileTreeView file browser and SDK CodeEditor
9+
- FileTreeView component: loading spinner state, empty placeholder, directories-before-files sorting, dirty dot indicators, fixed width with horizontal scroll
10+
- SDK CodeEditor with empty state (code icon, "Start editing"), language label, auto-selected handler file based on func.yaml runtime
11+
- EditToolbar with Back link (unsaved changes modal), Save & Deploy (success alert with 2s auto-dismiss, danger alert on error)
12+
- SourceControlService: fetch(repo) via git.getTree recursive + blob fetches, updateRepo(repo) with local commit SHA cache for GitHub's eventual consistency
13+
- Decomposed push() into push() (initial commits) and updateRepo() (subsequent commits)
14+
- Migrated test runner from Jest to Vitest with MSW 2.x for GitHub API mocking
15+
- Merged RepoInfo + SourceRepo into single RepoMetadata type
16+
- Extracted parseNamespaceAndRuntime, getLanguageFromPath, handlerMap into shared utils
17+
- Added CSP connect-src for GitHub API in dev mode (start-console.sh)
18+
- Removed navigation state passing between pages (each page loads own data from URL params)
19+
- Added CSS, co-location, React performance rules to style guide and architecture docs
20+
- Added communication rules and draft PR step to workflow docs
21+
- 12 test suites, 89 tests, all passing, zero lint errors
22+
Left off: PR #16 ready for review. Missing test coverage for dirty tracking, save error alert, modal interactions, file selection.
23+
Blockers: None
24+
425
---
526
## 2026-04-28 | Session: Runtime PAT entry and user avatar
627
Worked on: Replace compile-time PAT injection with runtime modal entry, add user avatar to page headers

docs/features.json

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,19 @@
9696
],
9797
"passes": true
9898
},
99+
{
100+
"category": "technical",
101+
"description": "CI/CD: GitHub Actions pipelines for PR checks and master publish to GHCR, plus lint fixes and README deployment instructions",
102+
"steps": [
103+
"PR pipeline (.github/workflows/pr.yml): triggers on pull_request to master, runs yarn install --immutable, yarn lint, yarn test — branch cannot merge without passing checks and an approval",
104+
"Publish pipeline (.github/workflows/publish.yml): triggers on push to master (merged PRs), runs yarn install --immutable, yarn lint, yarn test, then builds multi-arch container image via docker/build-push-action using existing Dockerfile and pushes to ghcr.io/twogiants/console-functions-plugin with :latest and :sha-<commit> tags",
105+
"Both pipelines use actions/setup-node@v4 with node-version 22, corepack enable, and cache: yarn for Yarn install caching (GitHub runners ship Node 20 by default with corepack disabled)",
106+
"Both pipelines authenticate to GHCR using GITHUB_TOKEN secret with packages:write permission (token added to repo's Actions secrets)",
107+
"Add yarn lint script to package.json if missing, run it, and fix all lint errors in the codebase",
108+
"Update README.md deployment section (lines 96-117) — replace generic Helm boilerplate, placeholder commands, and obsolete OCP 4.10 / i18n notes with concrete instructions: oc new-project console-functions-plugin, helm upgrade -i console-functions-plugin charts/openshift-console-plugin -n console-functions-plugin --create-namespace --set plugin.image=ghcr.io/twogiants/console-functions-plugin:latest@sha256:<digest>"
109+
],
110+
"passes": true
111+
},
99112
{
100113
"category": "functional",
101114
"description": "GitHub PAT Avatar: session-wide PAT entry via modal + avatar component showing authenticated GitHub user",
@@ -126,19 +139,6 @@
126139
],
127140
"passes": false
128141
},
129-
{
130-
"category": "technical",
131-
"description": "CI/CD: GitHub Actions pipelines for PR checks and master publish to GHCR, plus lint fixes and README deployment instructions",
132-
"steps": [
133-
"PR pipeline (.github/workflows/pr.yml): triggers on pull_request to master, runs yarn install --immutable, yarn lint, yarn test — branch cannot merge without passing checks and an approval",
134-
"Publish pipeline (.github/workflows/publish.yml): triggers on push to master (merged PRs), runs yarn install --immutable, yarn lint, yarn test, then builds multi-arch container image via docker/build-push-action using existing Dockerfile and pushes to ghcr.io/twogiants/console-functions-plugin with :latest and :sha-<commit> tags",
135-
"Both pipelines use actions/setup-node@v4 with node-version 22, corepack enable, and cache: yarn for Yarn install caching (GitHub runners ship Node 20 by default with corepack disabled)",
136-
"Both pipelines authenticate to GHCR using GITHUB_TOKEN secret with packages:write permission (token added to repo's Actions secrets)",
137-
"Add yarn lint script to package.json if missing, run it, and fix all lint errors in the codebase",
138-
"Update README.md deployment section (lines 96-117) — replace generic Helm boilerplate, placeholder commands, and obsolete OCP 4.10 / i18n notes with concrete instructions: oc new-project console-functions-plugin, helm upgrade -i console-functions-plugin charts/openshift-console-plugin -n console-functions-plugin --create-namespace --set plugin.image=ghcr.io/twogiants/console-functions-plugin:latest@sha256:<digest>"
139-
],
140-
"passes": true
141-
},
142142
{
143143
"category": "functional",
144144
"description": "Set GitHub Secret (KUBECONFIG) on created function repos so GH Actions can deploy to the cluster",
@@ -149,5 +149,23 @@
149149
"GH Actions workflow can authenticate to the cluster and run func deploy"
150150
],
151151
"passes": false
152+
},
153+
{
154+
"category": "functional",
155+
"description": "Function Edit Page with TreeView file browser and CodeEditor for editing and deploying function code",
156+
"steps": [
157+
"SourceControlService extended with fetch(repo) and updateRepo(repo) methods. fetch retrieves full repo tree via git.getTree recursive + blob fetches. updateRepo commits on existing branches with local commit SHA cache for GitHub's eventual consistency",
158+
"Navigate to /faas/edit/:name loads files from GitHub via listFunctionRepos + fetch (each page loads its own data from URL params)",
159+
"Page layout: toolbar at top (Back link left, success/danger Alert center, Save & Deploy button right), FileTreeView fixed-width left panel (16rem, horizontally scrollable), SDK CodeEditor fills remaining width (70vh height)",
160+
"FileTreeView builds nested tree from flat FileEntry[] paths with loading spinner state, empty placeholder state, directories expand/collapse only, files are selectable and update CodeEditor content",
161+
"SDK CodeEditor with empty state (code icon + 'Start editing' message) when no file selected, language label visible in header",
162+
"CodeEditor language auto-detected from file extension via getLanguageFromPath utility",
163+
"Handler file auto-selected on load based on runtime from func.yaml (no fallback selection if handler not found)",
164+
"Modified files indicated in tree with dot indicator after filename, dirty state derived by comparing working content against original per file on every onChange",
165+
"Save & Deploy button pushes all files to GitHub via updateRepo, resets dirty state on success, shows spinner and disables button during push, shows success alert with auto-dismiss after 2 seconds",
166+
"Back to Functions link navigates to /faas directly if clean, shows unsaved changes confirmation modal if dirty",
167+
"Empty tree with 'No files' placeholder and disabled Save & Deploy when repo not found, back link always visible"
168+
],
169+
"passes": true
152170
}
153171
]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Function Edit Page Implementation Plan
2+
3+
**Status:** COMPLETED
4+
5+
**Goal:** Build the Function Edit Page with a TreeView file browser and CodeEditor for editing and deploying function code to GitHub.
6+
7+
**Architecture:** The page follows the project's layered architecture. A `useFunctionEditPage` hook (co-located in the view file, unexported) handles data loading, state management, and save logic. A `FileTreeView` component with a co-located `useFileTreeView` hook renders the file tree. An `EditToolbar` component with `useEditToolbar` hook manages save flow and navigation. The `SourceControlService` interface is extended with `fetch()` and `updateRepo()`.
8+
9+
**Tech Stack:** React, PatternFly 6 (TreeView, Toolbar, Modal, Button, Alert, EmptyState), OCP Dynamic Plugin SDK (CodeEditor, DocumentTitle, ListPageHeader), Vitest + React Testing Library + MSW
10+
11+
**Testing approach:** MSW for all GitHub API interactions. `vi.mock` only for `react-i18next` and OCP SDK components.
12+
13+
---
14+
15+
## Completed Tasks
16+
17+
### Task 1: Add fetch() to SourceControlService and GithubService
18+
19+
- Added `fetch(repo)` using `git.getTree({ recursive: '1' })` + per-file `git.getBlob`
20+
- Migrated all GithubService tests from `vi.mock('@octokit/rest')` to MSW handlers
21+
- Renamed test file to `GithubService.test.ts`
22+
- Merged `RepoInfo` + `SourceRepo` into single `RepoMetadata` type
23+
- Added test for `fetchFileContent` error path
24+
25+
### Task 2: Decompose push() into push() and updateRepo()
26+
27+
- `push()` for initial commits (createRef, no parents)
28+
- `updateRepo()` for subsequent commits with local commit SHA cache
29+
- Cache handles GitHub's eventually consistent ref storage (stale getRef on rapid saves)
30+
- Cache clears only on "not a fast forward" errors (external push), not on network errors
31+
- 5 updateRepo tests: first push, second from cache, third from cache, stale cache recovery, network error preserves cache
32+
33+
### Task 3: Add getLanguageFromPath utility
34+
35+
- Maps file extensions and special filenames (Dockerfile, Makefile) to Monaco Language enum
36+
- Extracted to shared `src/utils/utils.ts` along with `parseNamespaceAndRuntime` and `handlerMap`
37+
38+
### Tasks 4+5: FileTreeView component
39+
40+
- Builds `TreeViewDataItem[]` directly from flat `FileEntry[]` paths (no intermediate type)
41+
- Separate handling for root files and nested files
42+
- Directories render before files, sorted alphabetically
43+
- Loading state with spinner and "Loading source..." text
44+
- Empty state with "No files" placeholder (not clickable)
45+
- Dirty indicator after filename (`func.yaml ●`)
46+
- `React.memo` with `useMemo` (justified: parent re-renders on every CodeEditor keystroke)
47+
- Fixed width (16rem) with horizontal scroll, vertical scroll for long file lists
48+
- 11 test cases using realistic Node function file structure from knative/func templates
49+
50+
### Task 6: FunctionEditPage view and hooks
51+
52+
- Page component: toolbar + flex layout (FileTreeView left, SDK CodeEditor right)
53+
- `Flex` with `direction: row`, `flexWrap: nowrap`, `alignItems: stretch` (fixes baseline alignment stacking issue)
54+
- SDK CodeEditor with `height="70vh"`, empty state (code icon + "Start editing"), language label visible
55+
- Handler file auto-selected based on runtime from func.yaml (function.go for Go, index.js for Node, function/func.py for Python)
56+
- Each page loads its own data from URL params (no navigation state passing between pages)
57+
- `resolveRepoContent` extracts API calls from state management
58+
- Repo metadata stored in state after load for save without re-fetching
59+
60+
### EditToolbar component
61+
62+
- Back link (left), success/danger Alert (center), Save & Deploy button (right)
63+
- `useEditToolbar` hook: save flow with isSaving/error/success state
64+
- Success alert "Pushed to GitHub. Deployment running..." with 2-second auto-dismiss (tested with `vi.useFakeTimers`)
65+
- Unsaved changes confirmation modal (LeaveModal) on back navigation when dirty
66+
- Save & Deploy disabled when no changes
67+
68+
### Infrastructure changes
69+
70+
- Migrated from Jest to Vitest with MSW 2.x
71+
- Custom jsdom environment removed (Vitest handles globals natively)
72+
- CSP connect-src for GitHub API added to start-console.sh for dev mode
73+
- Added CSS, co-location, React performance rules to style guide and architecture docs
74+
- Added communication rules and draft PR step to workflow docs
75+
76+
---
77+
78+
## Deviations from original plan
79+
80+
- Used SDK CodeEditor instead of PatternFly CodeEditor (CSP blob URL issues with Monaco workers in OCP Console)
81+
- Removed navigation state passing between list and edit page (each page is self-contained)
82+
- Split `push()` into `push()` + `updateRepo()` (clearer separation of initial vs subsequent commits)
83+
- Added local commit SHA cache (GitHub eventual consistency workaround)
84+
- Added success alert after save (not in original plan)
85+
- Editor empty state with code icon (not in original plan)
86+
- `autoSelectHandler` renamed to `determineHandler` (pure function, returns path instead of calling setState)

docs/potential-features.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,16 @@
2222
"Namespace column visible in admin perspective, hidden in dev perspective (redundant with project selector)"
2323
],
2424
"passes": false
25+
},
26+
{
27+
"category": "technical",
28+
"description": "Replace per-file blob fetching in GithubService.fetch() with single archive download (downloadTarballArchive) and extraction for better performance",
29+
"steps": [
30+
"Download repo as tarball via octokit.repos.downloadTarballArchive()",
31+
"Decompress gzip with DecompressionStream, parse tar format",
32+
"Strip the prefix directory from extracted paths",
33+
"Return FileEntry[] from extracted contents instead of N+1 getBlob calls"
34+
],
35+
"passes": false
2536
}
2637
]

jest.config.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

locales/en/plugin__console-functions-plugin.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"Delete": "Delete",
1414
"Edit": "Edit",
1515
"Enter your GitHub Personal Access Token to connect your repositories.": "Enter your GitHub Personal Access Token to connect your repositories.",
16+
"Edit function": "Edit function",
1617
"Error creating function": "Error creating function",
1718
"Function Settings": "Function Settings",
1819
"FaaS": "FaaS",
@@ -25,12 +26,21 @@
2526
"No functions found": "No functions found",
2627
"Owner": "Owner",
2728
"Personal Access Token": "Personal Access Token",
29+
"Pushed to GitHub. Deployment running...": "Pushed to GitHub. Deployment running...",
2830
"Registry": "Registry",
2931
"Replicas": "Replicas",
3032
"Repository": "Repository",
3133
"Runtime": "Runtime",
3234
"Serverless functions in your repository and deployed to your cluster. Manage lifecycle, monitor status, and scale on demand.": "Serverless functions in your repository and deployed to your cluster. Manage lifecycle, monitor status, and scale on demand.",
3335
"Status": "Status",
3436
"URL": "URL",
35-
"Undeploy": "Undeploy"
37+
"Undeploy": "Undeploy",
38+
"Back to Functions": "Back to Functions",
39+
"Save & Deploy": "Save & Deploy",
40+
"Select a file from the tree view to start editing.": "Select a file from the tree view to start editing.",
41+
"Start editing": "Start editing",
42+
"Unsaved changes": "Unsaved changes",
43+
"You have unsaved changes. Leave anyway?": "You have unsaved changes. Leave anyway?",
44+
"Stay": "Stay",
45+
"Leave": "Leave"
3646
}

0 commit comments

Comments
 (0)