|
17 | 17 | # API utilities & constants |
18 | 18 |
|
19 | 19 | - Treat `app/api/util.ts` (and friends) as a thin translation layer: mirror backend rules only when the UI needs them, keep the client copy minimal, and always link to the authoritative Omicron source so reviewers can verify the behavior. |
20 | | -- API constants live in `app/api/util.ts:25-38` with links to Omicron source: `MAX_NICS_PER_INSTANCE` (8), `INSTANCE_MAX_CPU` (64), `INSTANCE_MAX_RAM_GiB` (1536), `MIN_DISK_SIZE_GiB` (1), `MAX_DISK_SIZE_GiB` (1023), etc. |
21 | | -- Use `ALL_ISH` (1000) from `app/util/consts.ts` when UI needs "approximately everything" for non-paginated queries—convention is to use this constant rather than magic numbers. |
| 20 | +- API constants live in `app/api/util.ts` with links to Omicron source. |
22 | 21 |
|
23 | 22 | # Testing code |
24 | 23 |
|
|
31 | 30 |
|
32 | 31 | # Data fetching pattern |
33 | 32 |
|
34 | | -- Define endpoints with `apiq`, prefetch them in a `clientLoader`, then read data with `usePrefetchedQuery`. |
35 | | -- Use `ALL_ISH` when the UI needs every item (e.g. release lists) and rely on `queryClient.invalidateEndpoint`—it now returns the `invalidateQueries` promise so it can be awaited (see `app/pages/system/UpdatePage.tsx`). |
| 33 | +- Define queries with `q(api.endpoint, params)` for single items or `getListQFn(api.listEndpoint, params)` for lists. Prefetch in `clientLoader` and read with `usePrefetchedQuery`; for on-demand fetches (modals, secondary data), use `useQuery` directly. |
| 34 | +- Use `ALL_ISH` from `app/util/consts.ts` when UI needs "all" items. Use `queryClient.invalidateEndpoint` to invalidate queries. |
36 | 35 | - For paginated tables, compose `getListQFn` with `useQueryTable`; the helper wraps `limit`/`pageToken` handling and keeps placeholder data stable (`app/api/hooks.ts:123-188`, `app/pages/ProjectsPage.tsx:40-132`). |
37 | | -- When a loader needs dependent data, fetch the primary list with `queryClient.fetchQuery`, prefetch its per-item queries, and only await a bounded batch so render isn’t blocked (see `app/pages/project/affinity/AffinityPage.tsx`). |
| 36 | +- When a loader needs dependent data, fetch the primary list with `queryClient.fetchQuery`, prefetch its per-item queries, and only await a bounded batch so render isn't blocked (see `app/pages/project/affinity/AffinityPage.tsx`). |
38 | 37 |
|
39 | 38 | # Mutations & UI flow |
40 | 39 |
|
|
83 | 82 | # Layout & accessibility |
84 | 83 |
|
85 | 84 | - Build pages inside the shared `PageContainer`/`ContentPane` so you inherit the skip link, sticky footer, pagination target, and scroll restoration tied to `#scroll-container` (`app/layouts/helpers.tsx`, `app/hooks/use-scroll-restoration.ts`). |
86 | | -- Surface page-level buttons and pagination via the `PageActions` and `Pagination` tunnels from `tunnel-rat`; anything rendered through `.In` components lands in the footer `.Target` automatically (`app/components/PageActions.tsx`, `app/components/Pagination.tsx`). This tunnel pattern is preferred over React portals for maintaining component co-location. |
| 85 | +- Surface page-level buttons and pagination via the `PageActions` and `Pagination` tunnels from `tunnel-rat`; anything rendered through `.In` lands in `.Target` automatically. |
87 | 86 | - For global loading states, reuse `PageSkeleton`—it keeps the MSW banner and grid layout stable, and `skipPaths` lets you opt-out for routes with custom layouts (`app/components/PageSkeleton.tsx`). |
88 | 87 | - Enforce accessibility at the type level: use `AriaLabel` type from `app/ui/util/aria.ts` which requires exactly one of `aria-label` or `aria-labelledby` on custom interactive components. |
89 | 88 |
|
90 | 89 | # Route params & loaders |
91 | 90 |
|
92 | 91 | - Wrap `useParams` with the provided selectors (`useProjectSelector`, `useInstanceSelector`, etc.) so required params throw during dev and produce memoized results safe for dependency arrays (`app/hooks/use-params.ts`). |
93 | | -- Param selectors use React Query's `hashKey` internally to ensure stable object references across renders—same values = same object identity, preventing unnecessary re-renders. |
94 | | -- Prefer `queryClient.fetchQuery` inside `clientLoader` blocks when the page needs data up front, and throw `trigger404` on real misses so the shared error boundary can render Not Found or the 403 IDP guidance (`app/pages/ProjectsPage.tsx`, `app/layouts/SystemLayout.tsx`, `app/components/ErrorBoundary.tsx`). |
| 92 | +- Prefer `queryClient.fetchQuery` inside `clientLoader` blocks when the page needs data up front, and throw `trigger404` on real misses so the error boundary renders Not Found. |
95 | 93 |
|
96 | 94 | # Global stores & modals |
97 | 95 |
|
|
100 | 98 |
|
101 | 99 | # UI components & styling |
102 | 100 |
|
103 | | -- Reach for primitives in `app/ui` before inventing page-specific widgets; that directory intentionally holds router-agnostic building blocks (`app/ui/README.md`). |
| 101 | +- Reach for primitives in `app/ui` before inventing page-specific widgets; that directory holds router-agnostic building blocks. |
104 | 102 | - When you just need Tailwind classes on a DOM element, use the `classed` helper instead of creating one-off wrappers (`app/util/classed.ts`). |
105 | | -- Reuse utility components for consistent formatting—`TimeAgo`, `EmptyMessage`, `CardBlock`, `DocsPopover`, `PropertiesTable`, and friends exist so pages stay visually aligned (`app/components/TimeAgo.tsx`, `app/ui/lib`). |
106 | | - |
107 | | -# Docs & external links |
108 | | - |
109 | | -- Keep help URLs centralized: add new docs to `links`/`docLinks` and reference them when wiring `DocsPopover` or help badges (`app/util/links.ts`). |
| 103 | +- Reuse utility components for consistent formatting—`TimeAgo`, `EmptyMessage`, `CardBlock`, `DocsPopover`, `PropertiesTable`, etc. |
| 104 | +- Import icons from `@oxide/design-system/icons/react` with size suffixes: `16` for inline/table, `24` for headers/buttons, `12` for tiny indicators. |
| 105 | +- Keep help URLs in `links`/`docLinks` (`app/util/links.ts`). |
110 | 106 |
|
111 | 107 | # Error handling |
112 | 108 |
|
113 | | -- All API errors flow through `processServerError` in `app/api/errors.ts`, which transforms raw errors into user-friendly messages with special handling for common cases (Forbidden, ObjectNotFound, ObjectAlreadyExists). |
114 | | -- On 401 errors, requests auto-redirect to `/login?redirect_uri=...` except for `loginLocal` endpoint which handles 401 in-page (`app/api/hooks.ts:49-57`). |
115 | | -- On 403 errors, the error boundary automatically checks if the user has no groups and no silo role, displaying IDP misconfiguration guidance when detected (`app/components/ErrorBoundary.tsx:42-54`). |
116 | | -- Throw `trigger404` (an object `{ type: 'error', statusCode: 404 }`) in loaders when resources don't exist; the error boundary will render `<NotFound />` (`app/components/ErrorBoundary.tsx`). |
117 | | - |
118 | | -# Validation patterns |
119 | | - |
120 | | -- Resource name validation: use `validateName` from `app/components/form/fields/NameField.tsx:44-60` (max 63 chars, lowercase letters/numbers/dashes, must start with letter, must end with letter or number). This matches backend validation. |
121 | | -- Description validation: use `validateDescription` for max 512 char limit (`app/components/form/fields/DescriptionField.tsx`). |
122 | | -- IP validation: use `validateIp` and `validateIpNet` from `app/util/ip.ts` for IPv4/IPv6 and CIDR notation—regexes match Rust `std::net` behavior for consistency. |
123 | | -- All validation functions return `string | undefined` for react-hook-form compatibility. |
124 | | - |
125 | | -# Type utilities |
126 | | - |
127 | | -- Check `types/util.d.ts` for `NoExtraKeys` (catches accidental extra properties) and other type helpers. |
128 | | -- Prefer `type-fest` utilities for advanced type manipulation. |
129 | | -- Route param types in `app/util/path-params.ts` use `Required<Sel.X>` pattern to distinguish required path params from optional query params. |
130 | | - |
131 | | -# Utility functions |
132 | | - |
133 | | -- Check `app/util/*` for string formatting, date handling, math, IP parsing, arrays, and file utilities. Use existing helpers before writing new ones. |
134 | | - |
135 | | -# Icons & visual feedback |
136 | | - |
137 | | -- Import icons from `@oxide/design-system/icons/react` with size suffixes: `16` for inline/table use, `24` for headers/buttons, `12` for tiny indicators. |
138 | | -- Use `StateBadge` for resource states, `EmptyMessage` for empty states, `HL` for highlighted text in messages. |
| 109 | +- All API errors flow through `processServerError` in `app/api/errors.ts`, which transforms raw errors into user-friendly messages. |
| 110 | +- On 401 errors, requests auto-redirect to `/login`. On 403, the error boundary checks for IDP misconfiguration. |
| 111 | +- Throw `trigger404` in loaders when resources don't exist; the error boundary will render Not Found. |
139 | 112 |
|
140 | | -# Role & permission patterns |
| 113 | +# Utilities & helpers |
141 | 114 |
|
142 | | -- Role helpers in `app/api/roles.ts`: `getEffectiveRole` determines most permissive role from a list, `roleOrder` defines hierarchy (admin > collaborator > viewer). |
143 | | -- Use `useUserRows` hook to enrich role assignments with user/group names, sorted via `byGroupThenName` (groups first, then alphabetically). |
144 | | -- Use `useActorsNotInPolicy` to fetch users/groups not already in a policy (for add-user forms). |
145 | | -- Policy transformations: `updateRole` and `deleteRole` produce new policies immutably. |
146 | | -- Check `userRoleFromPolicies` to determine effective user role across multiple policies (e.g., project + silo). |
| 115 | +- Check `app/util/*` for string formatting, date handling, IP parsing, etc. Check `types/util.d.ts` for type helpers. |
| 116 | +- Use `validateName` for resource names, `validateDescription` for descriptions, `validateIp`/`validateIpNet` for IPs. |
| 117 | +- Role helpers live in `app/api/roles.ts`. |
0 commit comments