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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ This is the project map. Read this first, every session.
FaaS PoC UI for OpenShift Console — React + TypeScript + Webpack + PatternFly 6 + OCP Dynamic Plugin SDK.
See `docs/design/` for full design specs.

## Communication

- Ask before staring modifications of code.
- Ask before starting a new task or making a design decision.
- Once actively implementing, keep going without asking. Only stop to ask when blocked by sandbox, permissions, or ambiguous requirements.

## Writing Style

No em dashes (`—`). Use commas, periods, or parentheses instead.
Expand Down
18 changes: 14 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,23 @@ Arrows mean "imports / depends on."
- Services never import Components or Views
- No circular dependencies

## Page / Component / Hook Rules
## React

**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.
### Page / Component / Hook Rules

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

**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.
**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.

**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.

**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.

**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.

### Performance

- **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.

## Architectural Guidance

Expand Down
6 changes: 6 additions & 0 deletions docs/STYLEGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@
- **No `any` type**: Use proper TypeScript types.
- **No `console.log`**: Use structured approach if logging needed.
- **Naming**: `use*` for hooks, `*Service` for services, PascalCase for components/types.
- **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.

## Documentation

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

## CSS

- **PatternFly first**: Use PatternFly component props for all layout and spacing. Only fall back to custom CSS when PatternFly does not cover the need.
- **Relative units only**: When custom CSS is necessary, use `rem` and `em` for all sizing. Never use `px`.

## OCP Plugin Styling Constraints

The `.stylelintrc.yaml` enforces strict rules to prevent breaking the OpenShift Console:
Expand Down
2 changes: 1 addition & 1 deletion docs/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Every session, before doing any work:
After [Startup Sequence](#startup-sequence), work through the picked feature:

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`
2. **Branch** — create feature branch per [Branching](#branching) convention
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.
3. **Implement** — using `/executing-plans` skill
4. **Review** — code review using `/requesting-code-review` skill, fix found issues
5. **Manual Test** — use browser automation and validate it works in the browser
Expand Down
21 changes: 21 additions & 0 deletions docs/claude-progress.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
# Claude Progress Log
# Newest entries first. Agents: append your entry at the top after the header.

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

---
## 2026-04-28 | Session: Runtime PAT entry and user avatar
Worked on: Replace compile-time PAT injection with runtime modal entry, add user avatar to page headers
Expand Down
44 changes: 31 additions & 13 deletions docs/features.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@
],
"passes": true
},
{
"category": "technical",
"description": "CI/CD: GitHub Actions pipelines for PR checks and master publish to GHCR, plus lint fixes and README deployment instructions",
"steps": [
"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",
"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",
"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)",
"Both pipelines authenticate to GHCR using GITHUB_TOKEN secret with packages:write permission (token added to repo's Actions secrets)",
"Add yarn lint script to package.json if missing, run it, and fix all lint errors in the codebase",
"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>"
],
"passes": true
},
{
"category": "functional",
"description": "GitHub PAT Avatar: session-wide PAT entry via modal + avatar component showing authenticated GitHub user",
Expand Down Expand Up @@ -126,19 +139,6 @@
],
"passes": false
},
{
"category": "technical",
"description": "CI/CD: GitHub Actions pipelines for PR checks and master publish to GHCR, plus lint fixes and README deployment instructions",
"steps": [
"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",
"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",
"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)",
"Both pipelines authenticate to GHCR using GITHUB_TOKEN secret with packages:write permission (token added to repo's Actions secrets)",
"Add yarn lint script to package.json if missing, run it, and fix all lint errors in the codebase",
"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>"
],
"passes": true
},
{
"category": "functional",
"description": "Set GitHub Secret (KUBECONFIG) on created function repos so GH Actions can deploy to the cluster",
Expand All @@ -149,5 +149,23 @@
"GH Actions workflow can authenticate to the cluster and run func deploy"
],
"passes": false
},
{
"category": "functional",
"description": "Function Edit Page with TreeView file browser and CodeEditor for editing and deploying function code",
"steps": [
"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",
"Navigate to /faas/edit/:name loads files from GitHub via listFunctionRepos + fetch (each page loads its own data from URL params)",
"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)",
"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",
"SDK CodeEditor with empty state (code icon + 'Start editing' message) when no file selected, language label visible in header",
"CodeEditor language auto-detected from file extension via getLanguageFromPath utility",
"Handler file auto-selected on load based on runtime from func.yaml (no fallback selection if handler not found)",
"Modified files indicated in tree with dot indicator after filename, dirty state derived by comparing working content against original per file on every onChange",
"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",
"Back to Functions link navigates to /faas directly if clean, shows unsaved changes confirmation modal if dirty",
"Empty tree with 'No files' placeholder and disabled Save & Deploy when repo not found, back link always visible"
],
"passes": true
}
]
86 changes: 86 additions & 0 deletions docs/plans/completed/016-feat-function-edit-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Function Edit Page Implementation Plan

**Status:** COMPLETED

**Goal:** Build the Function Edit Page with a TreeView file browser and CodeEditor for editing and deploying function code to GitHub.

**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()`.

**Tech Stack:** React, PatternFly 6 (TreeView, Toolbar, Modal, Button, Alert, EmptyState), OCP Dynamic Plugin SDK (CodeEditor, DocumentTitle, ListPageHeader), Vitest + React Testing Library + MSW

**Testing approach:** MSW for all GitHub API interactions. `vi.mock` only for `react-i18next` and OCP SDK components.

---

## Completed Tasks

### Task 1: Add fetch() to SourceControlService and GithubService

- Added `fetch(repo)` using `git.getTree({ recursive: '1' })` + per-file `git.getBlob`
- Migrated all GithubService tests from `vi.mock('@octokit/rest')` to MSW handlers
- Renamed test file to `GithubService.test.ts`
- Merged `RepoInfo` + `SourceRepo` into single `RepoMetadata` type
- Added test for `fetchFileContent` error path

### Task 2: Decompose push() into push() and updateRepo()

- `push()` for initial commits (createRef, no parents)
- `updateRepo()` for subsequent commits with local commit SHA cache
- Cache handles GitHub's eventually consistent ref storage (stale getRef on rapid saves)
- Cache clears only on "not a fast forward" errors (external push), not on network errors
- 5 updateRepo tests: first push, second from cache, third from cache, stale cache recovery, network error preserves cache

### Task 3: Add getLanguageFromPath utility

- Maps file extensions and special filenames (Dockerfile, Makefile) to Monaco Language enum
- Extracted to shared `src/utils/utils.ts` along with `parseNamespaceAndRuntime` and `handlerMap`

### Tasks 4+5: FileTreeView component

- Builds `TreeViewDataItem[]` directly from flat `FileEntry[]` paths (no intermediate type)
- Separate handling for root files and nested files
- Directories render before files, sorted alphabetically
- Loading state with spinner and "Loading source..." text
- Empty state with "No files" placeholder (not clickable)
- Dirty indicator after filename (`func.yaml ●`)
- `React.memo` with `useMemo` (justified: parent re-renders on every CodeEditor keystroke)
- Fixed width (16rem) with horizontal scroll, vertical scroll for long file lists
- 11 test cases using realistic Node function file structure from knative/func templates

### Task 6: FunctionEditPage view and hooks

- Page component: toolbar + flex layout (FileTreeView left, SDK CodeEditor right)
- `Flex` with `direction: row`, `flexWrap: nowrap`, `alignItems: stretch` (fixes baseline alignment stacking issue)
- SDK CodeEditor with `height="70vh"`, empty state (code icon + "Start editing"), language label visible
- Handler file auto-selected based on runtime from func.yaml (function.go for Go, index.js for Node, function/func.py for Python)
- Each page loads its own data from URL params (no navigation state passing between pages)
- `resolveRepoContent` extracts API calls from state management
- Repo metadata stored in state after load for save without re-fetching

### EditToolbar component

- Back link (left), success/danger Alert (center), Save & Deploy button (right)
- `useEditToolbar` hook: save flow with isSaving/error/success state
- Success alert "Pushed to GitHub. Deployment running..." with 2-second auto-dismiss (tested with `vi.useFakeTimers`)
- Unsaved changes confirmation modal (LeaveModal) on back navigation when dirty
- Save & Deploy disabled when no changes

### Infrastructure changes

- Migrated from Jest to Vitest with MSW 2.x
- Custom jsdom environment removed (Vitest handles globals natively)
- CSP connect-src for GitHub API added to start-console.sh for dev mode
- Added CSS, co-location, React performance rules to style guide and architecture docs
- Added communication rules and draft PR step to workflow docs

---

## Deviations from original plan

- Used SDK CodeEditor instead of PatternFly CodeEditor (CSP blob URL issues with Monaco workers in OCP Console)
- Removed navigation state passing between list and edit page (each page is self-contained)
- Split `push()` into `push()` + `updateRepo()` (clearer separation of initial vs subsequent commits)
- Added local commit SHA cache (GitHub eventual consistency workaround)
- Added success alert after save (not in original plan)
- Editor empty state with code icon (not in original plan)
- `autoSelectHandler` renamed to `determineHandler` (pure function, returns path instead of calling setState)
11 changes: 11 additions & 0 deletions docs/potential-features.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,16 @@
"Namespace column visible in admin perspective, hidden in dev perspective (redundant with project selector)"
],
"passes": false
},
{
"category": "technical",
"description": "Replace per-file blob fetching in GithubService.fetch() with single archive download (downloadTarballArchive) and extraction for better performance",
"steps": [
"Download repo as tarball via octokit.repos.downloadTarballArchive()",
"Decompress gzip with DecompressionStream, parse tar format",
"Strip the prefix directory from extracted paths",
"Return FileEntry[] from extracted contents instead of N+1 getBlob calls"
],
"passes": false
}
]
14 changes: 0 additions & 14 deletions jest.config.ts

This file was deleted.

12 changes: 11 additions & 1 deletion locales/en/plugin__console-functions-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Delete": "Delete",
"Edit": "Edit",
"Enter your GitHub Personal Access Token to connect your repositories.": "Enter your GitHub Personal Access Token to connect your repositories.",
"Edit function": "Edit function",
"Error creating function": "Error creating function",
"Function Settings": "Function Settings",
"FaaS": "FaaS",
Expand All @@ -25,12 +26,21 @@
"No functions found": "No functions found",
"Owner": "Owner",
"Personal Access Token": "Personal Access Token",
"Pushed to GitHub. Deployment running...": "Pushed to GitHub. Deployment running...",
"Registry": "Registry",
"Replicas": "Replicas",
"Repository": "Repository",
"Runtime": "Runtime",
"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.",
"Status": "Status",
"URL": "URL",
"Undeploy": "Undeploy"
"Undeploy": "Undeploy",
"Back to Functions": "Back to Functions",
"Save & Deploy": "Save & Deploy",
"Select a file from the tree view to start editing.": "Select a file from the tree view to start editing.",
"Start editing": "Start editing",
"Unsaved changes": "Unsaved changes",
"You have unsaved changes. Leave anyway?": "You have unsaved changes. Leave anyway?",
"Stay": "Stay",
"Leave": "Leave"
}
Loading