|
| 1 | +# Deployment Preset Management UI Dev Plan |
| 2 | + |
| 3 | +## Spec Reference |
| 4 | +`.specs/FR-2750-deployment-revision-preset-ui/spec.md` |
| 5 | + |
| 6 | +## Epic: FR-2750 |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Three tasks, grouped by functional area. Task 1 and Task 2 are parallel-friendly once the Relay schema is clear (both depend on generated types but can be developed simultaneously). Task 3 depends on Task 2 because it reuses `DeploymentPresetDetailContent`. |
| 11 | + |
| 12 | +- **Task 1 — Admin tab + List page** (1차 마일스톤 core): Relay scaffolding + list table + tab wiring |
| 13 | +- **Task 2 — Admin create/edit/delete modal** (1차 마일스톤 core): full form, delete confirmation, extracted detail content |
| 14 | +- **Task 3 — User-facing selector detail** (2차 마일스톤): ⓘ button in launcher + read-only modal |
| 15 | + |
| 16 | +## Naming & Pattern Conventions |
| 17 | + |
| 18 | +- **Table components**: Name them `AdminDeploymentPresetTable` (i.e. `OOOTable` not `OOONodes`). May be kept as an inner component inside the page file if it is not reused elsewhere. |
| 19 | +- **customizeColumns pattern**: Follow `BAIUserNodes` pattern — base columns defined inside the component, `customizeColumns` prop for consumer overrides, `BAINameActionCell` with customizable action options. |
| 20 | +- **No unnecessary modal/content splitting**: Do NOT split a modal into `SettingModal` + `SettingModalContent` unless the content is genuinely reused elsewhere. Keep form logic inside the modal file. The one legitimate split is `DeploymentPresetDetailContent`, which IS reused by both the admin delete-confirm preview and the user-facing read-only modal. |
| 21 | + |
| 22 | +## Sub-tasks (Implementation Order) |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +### Task 1: Admin tab + List page |
| 27 | + |
| 28 | +**Was**: sub-tasks 1 + 2 + 3 |
| 29 | + |
| 30 | +**Changed files**: |
| 31 | +- `react/src/__generated__/` (auto-generated, tracked) |
| 32 | +- `react/src/pages/AdminDeploymentPresetListPage.tsx` (new — contains both the list query and the `AdminDeploymentPresetTable` inner component) |
| 33 | +- `react/src/pages/AdminServingPage.tsx` (tab wiring) |
| 34 | +- `resources/i18n/en.json` (all 21 locale files under `resources/i18n/` — add keys to `en.json` first, then run `fw:i18n-translator` skill to auto-translate remaining locales) (`adminDeploymentPreset.*` namespace, tab title) |
| 35 | + |
| 36 | +**Dependencies**: None (Relay scaffolding is included here) |
| 37 | + |
| 38 | +**Review complexity**: Medium |
| 39 | + |
| 40 | +**Description**: |
| 41 | + |
| 42 | +1. **Relay scaffolding** — Define in the colocated component file (or a shared fragment file): |
| 43 | + - List query: `deploymentRevisionPresets` connection with fields `name`, `description`, `runtimeVariantId`, `rank`, `cluster`, `resource`, `execution`, `deploymentDefaults`, `modelDefinition`, `presetValues`, `createdAt`, `updatedAt`, `runtimeVariant { id, name }`, `resourceSlots`. |
| 44 | + - Three mutations: `adminCreateDeploymentRevisionPreset`, `adminUpdateDeploymentRevisionPreset`, `adminDeleteDeploymentRevisionPreset` — matching `CreateDeploymentRevisionPresetInput` / `UpdateDeploymentRevisionPresetInput` from `data/schema.graphql`. |
| 45 | + - Fragment `DeploymentRevisionPresetDetailFragment` — covers all fields needed by both the admin list and the user-facing read-only modal (used in Task 3). |
| 46 | + - Run `pnpm run relay` to materialize types under `react/src/__generated__/`. |
| 47 | + |
| 48 | +2. **AdminDeploymentPresetListPage** — Mirror `AdminModelCardListPage.tsx` structure: |
| 49 | + - `useLazyLoadQuery` + `BAITable` + `BAIGraphQLPropertyFilter` + `BAIFetchKeyButton` + `useBAIPaginationOptionStateOnSearchParam` + `useBAISettingUserState('table_column_overrides.AdminDeploymentPresetListPage')`. |
| 50 | + - **Inner component naming**: `AdminDeploymentPresetTable` (not `DeploymentPresetNodes`). |
| 51 | + - **customizeColumns**: follow `BAIUserNodes` pattern — base columns defined in `AdminDeploymentPresetTable`, `customizeColumns` prop for consumer overrides. |
| 52 | + - Columns: Name (fixed-left, `BAINameActionCell` with Edit/Delete action options), Description, Runtime (`runtimeVariant.name`), Image (from `execution`), Cluster (mode × size), Replicas (default), Strategy, Rank, Created at. |
| 53 | + - Defaults hidden: Description, Cluster, Strategy, Rank. |
| 54 | + - Filter properties: `name` (string contains), `runtimeVariantId` (UUID). |
| 55 | + - Sortable: `name`, `rank`, `createdAt` → `DeploymentRevisionPresetOrderField`. |
| 56 | + - The page component is mounted as a tab body (not a route). |
| 57 | + |
| 58 | +3. **Tab wiring in AdminServingPage**: |
| 59 | + - `React.lazy(() => import('./AdminDeploymentPresetListPage'))`. |
| 60 | + - Gate on `isSuperAdmin && baiClient.supports('deployment-preset')`. Tab key: `deployment-presets`. |
| 61 | + - Fallback to `serving` tab when the user lacks access (plain effect on primitives is fine). |
| 62 | + - Wrap in `BAIErrorBoundary` + `Suspense fallback={<Skeleton active />}` matching the existing `model-store` branch pattern. |
| 63 | + |
| 64 | +--- |
| 65 | + |
| 66 | +### Task 2: Admin create/edit/delete modal |
| 67 | + |
| 68 | +**Was**: sub-tasks 4 + 5 |
| 69 | + |
| 70 | +**Changed files**: |
| 71 | +- `react/src/components/AdminDeploymentPresetSettingModal.tsx` (new) |
| 72 | +- `react/src/components/DeploymentPresetDetailContent.tsx` (new — pure read-only summary, no Modal chrome) |
| 73 | +- `react/src/pages/AdminDeploymentPresetListPage.tsx` (wire create/edit/delete triggers) |
| 74 | +- `resources/i18n/en.json` (all 21 locale files under `resources/i18n/` — add keys to `en.json` first, then run `fw:i18n-translator` skill to auto-translate remaining locales) (form labels, section titles, placeholders, required-field errors, delete-confirm copy, summary section labels) |
| 75 | + |
| 76 | +**Dependencies**: Task 1 (list page and Relay types exist) |
| 77 | + |
| 78 | +**Review complexity**: High (largest single piece; covers all form fields, two mutation paths, delete flow) |
| 79 | + |
| 80 | +**Description**: |
| 81 | + |
| 82 | +1. **AdminDeploymentPresetSettingModal** — `BAIModal` + Ant `Form`. Keep all form logic inside this file (do NOT split into SettingModal + SettingModalContent). |
| 83 | + - Sections per the spec: |
| 84 | + 1. **Basic info**: `name` (required), `description`, `runtimeVariantId` (required, Select from `runtimeVariants` query — reuse existing dropdown from `ServiceLauncherPageContent` runtime selector if applicable), `imageId` (required — reuse `ImageEnvironmentSelectFormItems` which already queries `image.id` UUID; pass `values.environments.image?.id` as `imageId` on submit). |
| 85 | + 2. **Cluster**: `clusterMode` (single-node / multi-node), `clusterSize`. |
| 86 | + 3. **Resources**: `resourceSlots`, `resourceOpts` (reuse session/service launcher patterns if applicable; compact JSON textarea with parse validation otherwise). |
| 87 | + 4. **Execution**: `startupCommand`, `bootstrapScript`, `environ`, `presetValues`. |
| 88 | + 5. **Deployment defaults**: `openToPublic`, `replicaCount`, `revisionHistoryLimit`, `deploymentStrategy`. |
| 89 | + 6. **Advanced**: `modelDefinition`. |
| 90 | + 7. **Edit-only**: `rank`. |
| 91 | + - Create vs. Update mode determined by whether a `presetId` prop is passed. `runtimeVariantId` and `imageId` are not in `UpdateDeploymentRevisionPresetInput` — render as read-only display in edit mode. |
| 92 | + - Apply `'use memo'`, plain handlers (no `useCallback`), antd v6 prop names. |
| 93 | + - `BAIButton` `action` prop on Save for automatic loading state. |
| 94 | + |
| 95 | +2. **Delete with typed confirmation** (wired in `AdminDeploymentPresetListPage`): |
| 96 | + - `BAIConfirmModalWithInput` (per `destructive-confirmation.md`). `confirmText` = preset name; OK disabled until typed. |
| 97 | + - On success: `commitDeleteMutation` + `updateFetchKey()`. On rejection: surface server error message verbatim; defer cascade UX to follow-up. |
| 98 | + - Show `DeploymentPresetDetailContent` inside the confirm modal as a preview of what will be deleted. |
| 99 | + |
| 100 | +3. **DeploymentPresetDetailContent** (the one legitimate split): |
| 101 | + - Pure read-only component. Takes a Relay fragment ref (`DeploymentRevisionPresetDetailFragment`). |
| 102 | + - Renders: name, description, rank, runtime, image, cluster, resource block (CPU / memory / GPU / shmem from `resourceSlots`), deployment defaults (replicas, limit, strategy, public). |
| 103 | + - No buttons, no edit affordance. Reused in Task 3's read-only modal. |
| 104 | + |
| 105 | +--- |
| 106 | + |
| 107 | +### Task 3: User-facing selector detail |
| 108 | + |
| 109 | +**Was**: sub-tasks 6 + 7 |
| 110 | + |
| 111 | +**Changed files**: |
| 112 | +- `react/src/components/ServiceLauncherPageContent.tsx` (⓪ Select + ⓘ button + modal mount) |
| 113 | +- `react/src/components/DeploymentPresetDetailModal.tsx` (new — thin `BAIModal` wrapper) |
| 114 | +- `resources/i18n/en.json` (all 21 locale files under `resources/i18n/` — add keys to `en.json` first, then run `fw:i18n-translator` skill to auto-translate remaining locales) (`modelService.DeploymentPreset`, tooltip text, modal title, close button) |
| 115 | + |
| 116 | +**Dependencies**: Task 2 (`DeploymentPresetDetailContent` must exist) |
| 117 | + |
| 118 | +**Review complexity**: Medium (touches a long/complex form file; the addition is read-only and does not wire preset values into the deployment payload — explicitly out of scope) |
| 119 | + |
| 120 | +**Description**: |
| 121 | + |
| 122 | +1. **ⓘ button in ServiceLauncherPageContent**: |
| 123 | + - Locate the form area near the existing `runtimeVariant` Form.Item. |
| 124 | + - Add a `Form.Item` (or wrapper) with a `Space.Compact` containing: a `Select` of presets (sourced via `availablePresets` connection, scoped by `runtimeVariant` if needed) + a `Button` with info icon (`InfoIcon` from lucide or `<InfoCircleOutlined />`). |
| 125 | + - `Button` disabled when no preset selected. On click: open `DeploymentPresetDetailModal`. |
| 126 | + - Gate the entire row behind `baiClient.supports('deployment-preset')`. |
| 127 | + - Pattern reference: `VFolderSelect.tsx` (Select + `Space.Compact` button row). |
| 128 | + - **Out of scope**: applying chosen preset values to form state. Selector is display/inspect only. |
| 129 | + |
| 130 | +2. **DeploymentPresetDetailModal**: |
| 131 | + - `BAIModal` with `footer={null}` (or single Close button), title = preset name. |
| 132 | + - Body = `DeploymentPresetDetailContent` from Task 2. |
| 133 | + - Loads selected preset by id via `useLazyLoadQuery` for `deploymentRevisionPreset(id: $id)` so it works even when Select options are paginated/truncated. Wrap in `Suspense` with skeleton fallback. |
| 134 | + - No Edit, no Delete, no destructive actions. |
| 135 | + |
| 136 | +--- |
| 137 | + |
| 138 | +## Dependency Graph |
| 139 | + |
| 140 | +``` |
| 141 | +Task 1 (Relay + List + Tab) ──────────────────────► Task 2 (Modals + DetailContent) |
| 142 | + │ │ |
| 143 | + │ │ |
| 144 | + └─────────────────── parallel-friendly ───────────────┘ |
| 145 | + │ |
| 146 | + ▼ |
| 147 | + Task 3 (Selector + Read-only Modal) |
| 148 | +``` |
| 149 | + |
| 150 | +- Task 1 and Task 2 are **parallel-friendly** once the schema/fragment shape is agreed upon. |
| 151 | +- Task 3 **cannot start** until `DeploymentPresetDetailContent` (Task 2) is available. |
| 152 | + |
| 153 | +Jira `blocks` links: |
| 154 | +- Task 1 blocks Task 2 (generated types needed) |
| 155 | +- Task 2 blocks Task 3 (DeploymentPresetDetailContent) |
| 156 | + |
| 157 | +--- |
| 158 | + |
| 159 | +## Risks and Open Questions |
| 160 | + |
| 161 | +1. **Runtime variant source query**: Sub-task 2 needs a Select source for `runtimeVariantId`. Plan: reuse the existing runtime selector from `ServiceLauncherPageContent` (lines ~1639–1677) if it wraps `runtimeVariants` with the same shape; otherwise use `runtimeVariants(filter: ..., limit: 100)` from `data/schema.graphql:13827`. |
| 162 | +2. **Delete-while-in-use behavior**: Until backend semantics are confirmed (cascade vs. reject), surface the server error message as-is. Worth a Jira comment on FR-2750 once verified. |
| 163 | +3. **`modelRuntimeConfig` / `modelMountConfig` / `extraMounts`**: explicitly out of scope per spec. Do not add form fields. |
| 164 | +4. **Capability key `deployment-preset`**: confirmed for backend 26.4.2+ per spec. Task 1 (tab) and Task 3 (selector row) must gate on `baiClient.supports('deployment-preset')`. |
| 165 | +5. **Preset application is out of scope**: Task 3 only adds the selector and ⓘ button. Applying preset values to form state is a follow-up belonging to a separate spec (the spec lists this as a non-goal). |
| 166 | + |
| 167 | +--- |
| 168 | + |
| 169 | +## Estimated Effort |
| 170 | + |
| 171 | +| Task | Complexity | Estimate | |
| 172 | +|------|-----------|----------| |
| 173 | +| Task 1: Admin tab + List page | Medium | 1.5 days | |
| 174 | +| Task 2: Admin create/edit/delete modal | High | 2–2.5 days | |
| 175 | +| Task 3: User-facing selector detail | Medium | 1 day | |
| 176 | + |
| 177 | +**Total**: ~4.5–5 days. Task 1 and Task 2 can run in parallel, compressing wall-clock time to ~3 days if two engineers work simultaneously. |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## Jira Sub-tasks |
| 182 | + |
| 183 | +| Task | Jira Key | URL | |
| 184 | +|------|----------|-----| |
| 185 | +| Task 1: Add Deployment Preset admin tab and list page | FR-2760 | https://lablup.atlassian.net/browse/FR-2760 | |
| 186 | +| Task 2: Add Deployment Preset create/edit/delete modal | FR-2761 | https://lablup.atlassian.net/browse/FR-2761 | |
| 187 | +| Task 3: Add Deployment Preset detail view in service launcher | FR-2762 | https://lablup.atlassian.net/browse/FR-2762 | |
| 188 | + |
| 189 | +Links: |
| 190 | +- FR-2760 blocks FR-2761 |
| 191 | +- FR-2761 blocks FR-2762 |
| 192 | +- All tasks relate to FR-2750 |
0 commit comments