From a9248a98a5cb8178e8f9740e5f7774525233c340 Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 29 Jun 2026 11:46:47 +0800 Subject: [PATCH] chore: add create-crud-page and form-patterns skills, refresh CLAUDE.md --- .claude/skills/create-crud-page/SKILL.md | 100 +++++++++++++++++++ .claude/skills/form-patterns/SKILL.md | 103 ++++++++++++++++++++ .gitignore | 2 +- CLAUDE.md | 116 ++++++++++++++++------- 4 files changed, 288 insertions(+), 33 deletions(-) create mode 100644 .claude/skills/create-crud-page/SKILL.md create mode 100644 .claude/skills/form-patterns/SKILL.md diff --git a/.claude/skills/create-crud-page/SKILL.md b/.claude/skills/create-crud-page/SKILL.md new file mode 100644 index 00000000..89e973a1 --- /dev/null +++ b/.claude/skills/create-crud-page/SKILL.md @@ -0,0 +1,100 @@ +--- +name: create-crud-page +description: Scaffold a CRUD list/table page module in the gpustack-ui monorepo. Use when creating a new page module, building a list/table page, adding a create/edit drawer, or setting up the components/config/forms/hooks/services structure for a feature. +--- + +# Create a CRUD Table Page + +## Inputs (do this first) + +- The argument passed to this skill is the **module name** (e.g. `/create-crud-page api-keys` → module `api-keys`). If no name was given, ask for it. +- **Always ask the user for the API documentation before generating any code**, even if a module name was provided: + + > Where is the API documentation for this module? (OpenAPI/Swagger URL, schema file path, or an interface description) + +- Wait for the answer, then read/fetch it. Derive `config/types.ts` (`FormData`, `ListItem`), the `services` request hooks, and form fields from that schema. Do not guess field names or endpoints — if the doc is missing details, ask. + +- **Also ask which form layout to scaffold:** + + > Should the form use tabs? (1) a plain form without tabs, or (2) a tabbed form + + Choose the form structure in section 3 accordingly. Default to **no tabs** unless the user picks tabs or the schema clearly has many grouped sections. + +--- + +Before anything: **reuse common `components`, `hooks`, and `utils` from `@gpustack/core-ui` whenever possible.** + +Reference implementation for sections below: `src/pages/model-routes`. + +## Module structure + +Create the module under `src/pages/{module}`: + +```text +{module} +├── components +├── config +├── forms +├── hooks +├── index.tsx +└── services +``` + +## 1. components + +Module-specific components. + +- The create/edit form component is named `add-xxx-modal.tsx` (repo convention — keep the `-modal` suffix even though it is built with `FormDrawer`). +- Use `FormDrawer` from `@gpustack/core-ui`. +- If a table cell's render logic/structure is complex, extract it into `xxx-cell.tsx`. + +## 2. config + +```text +config +├── index.ts # static configs & constants +└── types.ts # TypeScript types +``` + +Naming: form types → `FormData`; table list item types → `ListItem`. + +## 3. forms + +Main form component goes in `forms/index.tsx`. + +- **Complex interactions** (Form.Item split across components): create a dedicated Form Context and wrap with `FormContext.Provider`. +- **Tab-based forms**: use `ScrollSpyTabs` from `@gpustack/core-ui`, wrapping the `Form` or `FormContext.Provider`. Do not use tabs unless necessary. +- **Required-field validation**: use `getRuleMessage` for standard `input`/`select`. +- For cascading selectors and async race protection, follow the **form-patterns** skill. + +## 4. hooks + +- Table columns → `use-xxx-columns.tsx`. +- Open/close hooks for `add-xxx-modal.tsx` → `use-create-xxx.ts`. + +## 5. index.tsx (list page entry) + +- **Data fetching**: `useTableFetch` from `@gpustack/core-ui`. +- **Data display**: + - Standard table → Ant Design `Table`. Ref: `src/pages/users/index.tsx`. + - Expandable/collapsible rows → `Table` from `@gpustack/core-ui`. Ref: `src/pages/model-routes/index.tsx`. + - Card-style lists → use `InfiniteScrollerProvider`. Ref: `src/pages/backends/index.tsx`. + +## 6. services + +`request` is injected via a provider — do **not** create a centralized `apis` directory like in `gpustack-ui`. Define request hooks directly in `services`. + +- Use `useRequest` from `@gpustack/core-ui`, or `useQueryData` (same underlying method). +- Ref: `src/pages/gpu-service/storage-types/services/use-create-storage-type.ts`. + +## 7. Empty data + +- Page table lists → `NoResult`. +- Simple (non-page) tables → `Empty` with `image={Empty.PRESENTED_IMAGE_SIMPLE}`. + +## Common UI conventions + +- **Drawer/Modal open/close**: use `useBodyScroll` from `@gpustack/core-ui`. Ref: `src/pages/model-routes/hooks/use-create-route.ts`. +- **Status display** (success/failed/processing/warning): use `StatusTag`. Ref: `src/pages/llmodels/components/table-list.tsx`. +- **Permission-gated visibility**: use `Access` / `useAccess`. Ref: `src/pages/access/index.tsx`. +- **Styles**: avoid `styled-components` for complex/large styling. Prefer `createStyles` for component-scoped dynamic styles, CSS Modules (`xxx.module.less`) for static structured styles. diff --git a/.claude/skills/form-patterns/SKILL.md b/.claude/skills/form-patterns/SKILL.md new file mode 100644 index 00000000..8b563c13 --- /dev/null +++ b/.claude/skills/form-patterns/SKILL.md @@ -0,0 +1,103 @@ +--- +name: form-patterns +description: Patterns for forms with cascading/dependent selections in the gpustack-ui monorepo. Use when building a form where picking one field derives another (pick A → auto-pick B → write form), handling async option loading on modal open, or protecting against stale async results. +--- + +# Form Patterns + +Theme: keep cascading selections (pick A → derive B → write form) on a single, predictable path. + +## Accessing the form: `form` vs `form.current` + +This is **not** absolute — it depends on the call site: + +- **Inside the form component** (`forms/index.tsx`), or anywhere holding a `Form.useForm()` instance → call it directly: `form.setFieldsValue(...)`. +- **In the outer Drawer/Modal wrapper** that opens the form and holds it via `ref={form}` (`const form = useRef(null)`), driven by an `open` prop → go through the ref: `form.current?.setFieldsValue(...)`. + +The reference template below is written for the **Drawer-wrapper scenario** (it reacts to `open` and owns the shared `selection` state), so it uses `form.current?` throughout. If you lift this logic into the form body with a `useForm()` instance, drop the `.current`. + +## 1. No fallback for derived selection + +When "pick A then auto-pick B", match by rule and return `undefined` if no match — let the form field stay empty. Do **not** silently fall back to `list[0]`; a fallback hides data issues and fakes a valid selection. + +```ts +const findB = (key, list) => + key ? list.find((x) => x.key === key) : undefined; +``` + +For form fields, clear with `undefined`, not `''`. In Ant Design `undefined` restores the placeholder; `''` is treated as a real value. + +## 2. Async race protection + +For fetches triggered by a lifecycle entry (e.g. modal open), tag each invocation with a session ref. Discard stale results if the session rotated (modal closed and re-opened) before the response arrives. + +```ts +const sessionRef = useRef(0); + +useEffect(() => { + if (!open) { + sessionRef.current += 1; + return; + } + const session = ++sessionRef.current; + Promise.all([fetchA(), fetchB()]).then(([as, bs]) => { + if (sessionRef.current !== session) return; + applySelection(as.items[0], findB(as.items[0].key, bs.items)); + }); +}, [open]); +``` + +## 3. Reference template + +Two cascading selectors backed by a single shared state, with a single atomic write (state + form together): + +```ts +type Selection = { a?: string; b?: number }; + +const [selection, setSelection] = useState({}); +const sessionRef = useRef(0); +const form = useRef(null); // wrapper holds the form via
— see "Accessing the form" above + +const findB = (key, list) => + key ? list.find((x) => x.key === key) : undefined; + +// Single atomic write: state + form together. +const applySelection = (a, b) => { + setSelection({ a: a.name, b: b?.id }); + form.current?.setFieldsValue({ + field: b?.field, + spec: { ...currentSpec, ...b?.spec } + }); +}; + +// Trigger 1: modal opened +useEffect(() => { + if (!open) { + sessionRef.current++; + setSelection({}); + return; + } + const session = ++sessionRef.current; + Promise.all([fetchA(), fetchB()]).then(([as, bs]) => { + if (sessionRef.current !== session) return; + const first = as.items[0]; + applySelection(first, findB(first.key, bs.items)); + }); +}, [open]); + +// Trigger 2: user picks A +const handleAChange = (a) => { + applySelection(a, findB(a.key, listB)); +}; + +// Trigger 3: user picks B +const handleBChange = (b) => { + setSelection((prev) => ({ ...prev, b: b.id })); + form.current?.setFieldsValue({ ...b.fields }); +}; +``` + +## Related + +- Module/file structure for forms lives in the **create-crud-page** skill (section 3). +- Required-field validation: use `getRuleMessage`. diff --git a/.gitignore b/.gitignore index ebf98a10..6174ae11 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,6 @@ .swc .DS_Store .idea -.claude +.claude/settings.local.json /dist.zip .cache \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index d59966df..ac3df51c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,43 +1,95 @@ - -# GitNexus — Code Intelligence +# Repo -This project is indexed by GitNexus as **gpustack-ui** (7110 symbols, 13431 relationships, 235 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This is the **open source UI** (`gpustack-ui`). Common `components`, `hooks`, and `utils` are published as `@gpustack/core-ui` and consumed throughout `src`. -> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939). +**Always prioritize reusing common `components`, `hooks`, and `utils` from `@gpustack/core-ui`.** -## Always Do +Task-specific conventions live in skills: use **create-crud-page** when building a page module, **form-patterns** when building cascading/dependent forms. -- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. -- **MUST run `detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: `detect_changes({scope: "compare", base_ref: "main"})`. -- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. -- When exploring unfamiliar code, use `query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. -- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`. +# React State and Request Patterns -## Never Do +Keep data flow explicit, predictable, and performant. The triggering **action** is the source of truth for UI updates — not effect-driven synchronization. -- NEVER edit a function, class, or method without first running `impact` on it. -- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. -- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph. -- NEVER commit changes without running `detect_changes()` to check affected scope. +## 1. Avoid effect-driven requests -## Resources +Do not use request functions as `useEffect` dependencies. Trigger requests explicitly from user actions or lifecycle entry points. -| Resource | Use for | -|----------|---------| -| `gitnexus://repo/gpustack-ui/context` | Codebase overview, check index freshness | -| `gitnexus://repo/gpustack-ui/clusters` | All functional areas | -| `gitnexus://repo/gpustack-ui/processes` | All execution flows | -| `gitnexus://repo/gpustack-ui/process/{name}` | Step-by-step execution trace | +```ts +// Avoid +useEffect(() => { + fetchData(); +}, [fetchData]); +``` -## CLI +## 2. Form requests should be action-driven -| Task | Read this skill file | -|------|---------------------| -| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | -| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | -| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | -| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | -| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | -| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | +- Fetch form data (e.g. `Select` options) when the form first opens. +- If later requests depend on interactions, trigger them inside the interaction handler. +- Do not rely on `useEffect` dependency changes. - \ No newline at end of file +```ts +// Recommended +const handleOnChange = (value) => { + fetchData(value); +}; +``` + +## 3. Update related states together + +When one action updates multiple related states, update them all directly in the handler. Do not sync via `useEffect` or derive indirectly via `useMemo`. + +```ts +const handleOnChange = (value) => { + setState1(...); + setState2(...); + buildState(...); +}; +``` + +## 4. Group strongly related state + +If multiple states always update together, use a single state object instead of multiple `useState` calls — fewer rerenders, more predictable transitions. + +```ts +const [state, setState] = useState({ state1: ..., state2: ..., state3: ... }); +``` + +## 5. Prefer explicit state flow + +Keep request execution, state updates, and derived calculations close to the triggering action. Avoid chaining business logic through multiple `useEffect` hooks. + +```ts +// Prefer +const handleAction = () => { + fetchData(); + setTableData(...); + setSelectedRow(...); +}; +``` + +## 6. Avoid premature memoization + +Do not use `useMemo` / `useCallback` unless there is a confirmed bottleneck. Overuse adds complexity, obscures state flow, and risks stale dependencies. Optimize only when necessary. + +## 7. Keep request logic predictable + +A user interaction should clearly show: what request fires, which states update, how the UI changes. Avoid indirect update chains from dependency-driven effects. + +## 8. Prefer action-driven architecture + +Prefer action-driven updates, explicit handlers, and localized state transitions over effect-driven synchronization, cross-hook implicit updates, and reactive chains between states. + +# Styles + +Avoid `styled-components` for complex or large-scale styling. Prefer: + +1. `createStyles` for component-scoped dynamic styles +2. CSS Modules (`xxx.module.less`) for structured static styles + +# Common components + +- **Drawer/Modal open/close**: `useBodyScroll` from `@gpustack/core-ui`. +- **Status display** (success/failed/processing/warning): `StatusTag`. +- **Permission-gated visibility**: `Access` / `useAccess`. +- **Request hooks**: `useRequest` / `useQueryData` / `useQueryDataList` from `@gpustack/core-ui`. +- **Table data fetching**: `useTableFetch` from `@gpustack/core-ui`.