From 5cd5fbce63f27c0bb071b7ddbc948a0fafa3d060 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Tue, 10 Nov 2026 23:18:49 +0800 Subject: [PATCH 1/3] feat(web): pick a site profile preset on deploy; edit the release's affinity and tolerations on upgrade Deploy, in both modules, offers the site profile's presets. The picked one is merged into the form layer the web builds: maps merge, a list keeps both sides' items once, and a value the form sets wins. An upgrade rebuilds the whole form layer: the release's own, with the form's values over it. Every affinity rule in affinity and in each pd..affinityReplace shows as YAML to edit or remove, and the tolerations come back exactly as deployed, keys no field shows included. A list emptied of rules goes as [] or, where that is not valid, null, since swissd still merges an upgrade over the release's form layer. The site profile pages list the presets. --- .../modules/inferences/SiteProfilePage.tsx | 14 ++ .../inferences/components/DeployForm.tsx | 70 +++++++- .../inferences/components/DeploySheet.tsx | 6 +- .../modules/inferences/deployment-settings.ts | 3 +- web/src/modules/inferences/locales/en-US.json | 11 ++ web/src/modules/inferences/locales/zh-CN.json | 11 ++ .../swiss/components/DeploySettings.tsx | 154 +++++++++++++++--- web/src/modules/swiss/lib/affinity.ts | 95 +++++++++++ web/src/modules/swiss/lib/api.ts | 7 + web/src/modules/swiss/lib/formLayer.test.ts | 146 +++++++++++++++++ web/src/modules/swiss/lib/formLayer.ts | 48 ++++++ web/src/modules/swiss/routes/Deploy.tsx | 3 + web/src/modules/swiss/routes/SiteProfile.tsx | 22 +++ web/src/modules/swiss/routes/Upgrade.tsx | 4 +- 14 files changed, 559 insertions(+), 35 deletions(-) create mode 100644 web/src/modules/swiss/lib/affinity.ts create mode 100644 web/src/modules/swiss/lib/formLayer.test.ts create mode 100644 web/src/modules/swiss/lib/formLayer.ts diff --git a/web/src/modules/inferences/SiteProfilePage.tsx b/web/src/modules/inferences/SiteProfilePage.tsx index 7fc12ca..4a9f86e 100644 --- a/web/src/modules/inferences/SiteProfilePage.tsx +++ b/web/src/modules/inferences/SiteProfilePage.tsx @@ -227,6 +227,20 @@ function ProfileView({ p }: { p: SiteProfile }) { "sites", p.sites.map((s) => ({ label: s.name, value: mono(s.url) })), )} + {p.presets && Object.keys(p.presets).length > 0 && ( + +

{t("profile.hints.presets")}

+ {Object.entries(p.presets).map(([name, preset]) => ( +
+
+ {name} + {preset.description && {preset.description}} +
+ +
+ ))} +
+ )} {p.extra && Object.keys(p.extra).length > 0 && ( diff --git a/web/src/modules/inferences/components/DeployForm.tsx b/web/src/modules/inferences/components/DeployForm.tsx index 02c5e8b..fdd38bc 100644 --- a/web/src/modules/inferences/components/DeployForm.tsx +++ b/web/src/modules/inferences/components/DeployForm.tsx @@ -3,6 +3,7 @@ import { Badge, Button, Checkbox, + CodeBlock, DataSelect, DropdownMenu, DropdownMenuContent, @@ -23,12 +24,14 @@ import { cn, } from "@modelsphere/ui"; import { Plus, RefreshCw, X } from "lucide-react"; -import type { ClusterInfo } from "@swiss/lib/api"; +import type { ClusterInfo, Preset } from "@swiss/lib/api"; +import { editRule, listLabel, type AffinityRule } from "@swiss/lib/affinity"; +import { toYaml } from "@swiss/lib/yaml"; import { cacheFieldError, type CacheCard, type CacheDefaults } from "@swiss/lib/cache"; import { unlistedProducts, type GPUCapacity } from "@swiss/lib/gpu"; import { CHECK_MS, inputError, rememberable, type InputRow } from "@swiss/lib/inputs"; import { useDebounced } from "@swiss/lib/useDebounced"; -import { effective, imageChoices, type Form, type Toleration } from "@swiss/components/DeploySettings"; +import { effective, imageChoices, tolerationExtras, type Form, type Toleration } from "@swiss/components/DeploySettings"; import { MAX_VALUES, rejectKey } from "@swiss/lib/routeDirectives"; import { FALLBACK_ROUTE_OPTIONS, routeDirectives, runtimeValue, type RouteDirective, type RouteOptions } from "@/modules/inferences/routeOptions"; import { useT } from "@/modules/inferences/i18n"; @@ -67,9 +70,11 @@ export interface DeployFormProps { // Which typed answers to save to the site profile once the deploy applies; absent where it cannot be saved. remember?: Record; onRemember?: (key: string, on: boolean) => void; + // The site profile's presets, on a deploy. + presets?: Record; } -export function DeployForm({ form, onChange, upgrade, submitted, target, cluster, defaults, gpuOptions, gpuListed, gpuResources, capacity, expanded, onExpand, idPrefix, routeOptions, catalogRoute, helmValues, recovery, inputs, remember, onRemember }: DeployFormProps) { +export function DeployForm({ form, onChange, upgrade, submitted, target, cluster, defaults, gpuOptions, gpuListed, gpuResources, capacity, expanded, onExpand, idPrefix, routeOptions, catalogRoute, helmValues, recovery, inputs, remember, onRemember, presets }: DeployFormProps) { const t = useT(); const checked = useDebounced(form.inputs, CHECK_MS); const on = effective(form); @@ -108,6 +113,7 @@ export function DeployForm({ form, onChange, upgrade, submitted, target, cluster onChange({ serviceId })} suggestion={defaults.serviceId} disabled={upgrade} /> + {presets && Object.keys(presets).length > 0 && onChange({ preset })} />} {capacity && } onChange({ tolerations: v })} /> + {form.affinity.length > 0 && onChange({ affinity })} />}
onChange({ priorityClassName })} suggestion={cluster?.priorityClassName} /> @@ -396,13 +403,14 @@ function Tolerations({ rows, onChange }: { rows: Toleration[]; onChange: (v: Tol {rows.map((r, i) => (
- patch(i, { key: e.target.value })} placeholder={f("tolKey")} aria-label={f("tolKey")} /> + patch(i, { key: e.target.value })} placeholder={r.raw && !r.raw.key ? f("tolAny") : f("tolKey")} aria-label={f("tolKey")} /> patch(i, { operator: v as Toleration["operator"], value: v === "Exists" ? "" : r.value })} options={["Equal", "Exists"].map((o) => ({ value: o, label: o }))} /> patch(i, { value: e.target.value })} placeholder={f("tolValue")} aria-label={f("tolValue")} disabled={r.operator === "Exists"} /> patch(i, { effect: v })} options={EFFECTS.map((e) => ({ value: e, label: e || f("anyEffect") }))} /> + {tolerationExtras(r).length > 0 && {tolerationExtras(r).join(" · ")}}
))} +
+