---
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`, never `Tag` from `antd` directly. See **Status display** below. 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.

## Status display

Use `StatusTag` from `@gpustack/core-ui` for status display. Do not use `Tag` from `antd` directly.

1. Define the mapping from business status values to UI status values in the module's `config/index.ts`:

```ts
import { StatusMaps } from '@/config';
import { StatusType } from '@/config/types';

export const XxxStatusValueMap = {
  Running: 'running',
  Pending: 'pending',
  Failed: 'failed'
};

export const XxxStatusLabelMap: Record<string, string> = {
  [XxxStatusValueMap.Running]: 'Running',
  [XxxStatusValueMap.Pending]: 'Pending',
  [XxxStatusValueMap.Failed]: 'Failed'
};

export const status: Record<string, StatusType> = {
  [XxxStatusValueMap.Running]: StatusMaps.success,
  [XxxStatusValueMap.Pending]: StatusMaps.transitioning,
  [XxxStatusValueMap.Failed]: StatusMaps.error
};
```

2. In table columns, pass only the UI status value and display text required by `StatusTag`:

```tsx
<StatusTag
  statusValue={{
    status: status[value],
    text: XxxStatusLabelMap[value] || value,
    message: record.state_message
  }}
/>
```

`statusValue.status` must be a value mapped from `StatusMaps` (`success`, `transitioning`, `warning`, `error`, `inactive`). Do not pass business status values such as `running` or `pending` directly.
