diff --git a/.agents/skills/how-to-write-component/SKILL.md b/.agents/skills/how-to-write-component/SKILL.md
index f7e6e595092..8a480c8fd09 100644
--- a/.agents/skills/how-to-write-component/SKILL.md
+++ b/.agents/skills/how-to-write-component/SKILL.md
@@ -37,12 +37,16 @@ Use this as the decision guide for React/TypeScript component structure. Existin
- Do not replace prop drilling with one top-level hook that returns a large view model and then thread that object through section props. Move each hook, query, derived value, and handler to the concrete section that consumes it, or use feature-scoped Jotai atoms for simple shared form/UI state when siblings need the same source of truth.
- When using feature-scoped Jotai state for a form, drawer, or other secondary surface, scope the store to that surface instance when stale cross-instance state is possible. Initialize stable config at the owning boundary, then let descendants read only the atoms or purpose-named hooks they actually need.
- For Jotai-backed surfaces, put shared query atoms, mutation atoms, derived state, and write actions in the feature state file when they coordinate multiple descendants. The lowest-owner rule still applies to independent visual surfaces that do not participate in shared state.
+- For repeated row/menu action surfaces that need reset, hydrate the stable identity at the surface entry and scope only the primitives that truly need per-instance reset, such as open flags, drafts, or selected local options.
- Keep callbacks in a parent only for workflow coordination such as form submission, shared selection, batch behavior, or navigation. Otherwise let the child or row own its action.
- Prefer uncontrolled DOM state and CSS variables before adding controlled props.
## Feature-Scoped Jotai State
- A module's feature-local state lives in one state file for Jotai-backed features: primitive atoms, query atoms, derived atoms, write-only action atoms, mutation atoms, submission orchestration, provider exports, and optional scope configuration.
+- Keep state local when one component owns it, even inside Jotai-backed features. Dialog open flags, menu/popover visibility, confirmation visibility, form/input drafts, row-local pending flags, and in-flight refs usually belong in component state.
+- Promote UI state to an atom only when siblings need the same source of truth, the value drives a query or mutation atom, a parent workflow coordinates the state, or the state intentionally persists across hidden or unmounted descendants within a scoped surface.
+- Reflect atom-backed surface-wide locks or invariants in every affected trigger. If only one row, menu, or dialog should be disabled, keep the pending or lock state local to that row, menu, or dialog.
- Atom order in the state file follows the dependency graph: types/constants, editable primitives, query atoms, query-data derived atoms, readiness/business derived atoms, write actions, mutation atoms, submission orchestration, provider exports.
- Derived atom names read as business facts. Write atom names read as user or workflow commands.
- UI components read and write the exact atom they use with `useAtomValue` or `useSetAtom`. Repeated workflow semantics live in named derived atoms or write atoms.
@@ -51,7 +55,11 @@ Use this as the decision guide for React/TypeScript component structure. Existin
- Avoid feature hooks that aggregate form values, query results, derived state, and commands for sibling components. Prefer named derived atoms and write atoms so UI components read the exact shared fact or command they need.
- When a form library owns validation, keep submit orchestration in feature state when post-submit result or error state is shared by the surface. Avoid duplicating validation gates or request shaping in UI hooks.
- `jotai-tanstack-query` atoms use the same QueryClient as the React Query provider. Query atoms belong in feature state when atoms are the feature's local state surface.
-- Jotai scope is an optional instance-isolation tool for secondary surfaces with independent local state. Query atoms keep shared cache behavior through the shared QueryClient.
+- Jotai scope is an optional instance-isolation tool for secondary surfaces with independent local state. Query and mutation atoms keep shared cache behavior through the shared QueryClient.
+- Do not put `atomWithQuery`, `atomWithInfiniteQuery`, `atomWithMutation`, or broad derived orchestration atoms in a `ScopeProvider` just to reset a surface. Scoped derived atoms implicitly scope their dependencies, which can duplicate query client access and break shared invalidation. Leave query/mutation atoms unscoped; let them read scoped primitive inputs.
+- Scope providers should list resettable primitive atoms and explicit hydration tuples. If a derived atom must be scoped, confirm that every dependency it implicitly scopes is meant to be private to that surface.
+- Keep independent dialog lifecycles separate. Avoid a single discriminated "current action dialog" atom when edit, delete, and other dialogs have their own open state, loading guard, or reset behavior.
+- Route-derived stable identities that do not need instance reset or scoped isolation can be hydrated at the route or layout boundary into a feature route atom. Use scoped atoms only when stale cross-instance state or per-surface reset semantics are needed.
## Components, Props, And Types
@@ -74,6 +82,7 @@ Use this as the decision guide for React/TypeScript component structure. Existin
- Use generated enum objects and union types directly in props, comparisons, status logic, and i18n keys. Do not add local enum constants or parallel frontend enum/status layers unless they model real product state not represented by the API. Presentation-only tone maps should be keyed by the generated enum.
- Normalize or coerce only at a real boundary, such as user-entered forms, search, URL/query params, file names, DOM IDs, or legacy adapters. Preserve user-entered values when whitespace or formatting can be meaningful.
- Do not coerce nullable or optional API strings to `''` in query, derived model, or payload-building code. Keep `undefined` or `null` until the final boundary that requires a string.
+- Do not use `value || undefined` for mutation payload fields where an empty string means "clear this value". Trim or normalize at the form boundary, then preserve `''` when the API contract treats it as an intentional update.
- Local UI models are fine for presentation, form state, select options, or guarded required-field refinements. Name them as UI concepts, not generated DTO mirrors.
- Required-value refinements are allowed only after same-branch filtering or early return. Prefer nullable-tolerant props for render-only data.
- When a component needs a stricter shape than a generated DTO, refine once at the API/query-to-UI boundary into a purpose-named UI type instead of hiding missing fields with generic fallback or coercion helpers.
@@ -93,12 +102,17 @@ Use this as the decision guide for React/TypeScript component structure. Existin
- Keep `web/contract/*` as the single source of truth for API shape; follow existing domain/router patterns and the `{ params, query?, body? }` input shape.
- Consume queries directly with `useQuery(consoleQuery.xxx.queryOptions(...))` or `useQuery(marketplaceQuery.xxx.queryOptions(...))`.
+- In `atomWithQuery` and `atomWithInfiniteQuery`, return generated `queryOptions()` or `infiniteOptions()` directly. Pass `enabled`, `retry`, `placeholderData`, `select`, and pagination options into that call instead of spreading generated options into a hand-built object.
+- In `atomWithMutation`, return generated `mutationOptions()` directly when using generated clients. Put request shaping and submit orchestration in write atoms; do not rebuild mutation option objects just to pass through the generated mutation function.
+- For custom query functions that do not come from generated clients, wrap the options object with TanStack `queryOptions(...)` so query atoms still return a query options contract.
- Avoid pass-through hooks and thin `web/service/use-*` wrappers that only rename `queryOptions()` or `mutationOptions()`. Extract a small `queryOptions` helper only when repeated call-site options justify it.
- Keep feature hooks for real orchestration, workflow state, or shared domain behavior.
- For TanStack cache data, use generated or query-derived types; do not create local wrappers for `getQueryData` or `getQueriesData`.
-- For generated oRPC `queryOptions()` / `infiniteOptions()`, do not pass `skipToken` as `input`; keep a valid placeholder input shape and use `enabled` to gate missing required params because the OpenAPI codec encodes input eagerly.
+- For generated oRPC `queryOptions()` / `infiniteOptions()`, keep returning the generated options directly. When required input is missing, use a whole-input branch such as `input: condition ? validInput : skipToken` together with `enabled: Boolean(condition)` so no request runs and no fake payload is built.
+- Do not put `skipToken` inside a nested placeholder payload, such as `{ params: { appInstanceId: skipToken } }`. Do not create hand-written "missing queryOptions" objects or coerce required IDs to `''`.
- Consume mutations directly with `useMutation(consoleQuery.xxx.mutationOptions(...))` or `useMutation(marketplaceQuery.xxx.mutationOptions(...))`; use oRPC clients as `mutationFn` only for custom flows.
- Put shared cache behavior in `createTanstackQueryUtils(...experimental_defaults...)`; components may add UI feedback callbacks, but should not own shared invalidation rules.
+- Component or atom mutation callbacks can handle local UI feedback such as toasts, closing dialogs, or navigation. They should not replace shared invalidation or add local cache patches for shared server state.
- Do not use deprecated `useInvalid` or `useReset`.
- Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required, and wrap awaited calls in `try/catch`.
@@ -110,6 +124,7 @@ Use this as the decision guide for React/TypeScript component structure. Existin
- Keep cohesive forms, menu bodies, and one-off helpers local unless they need their own state, reuse, or semantic boundary.
- Separate hidden secondary surfaces from the trigger's main flow. For dialogs, dropdowns, popovers, and similar branches, extract a small local component that owns the trigger, open state, and hidden content when it would obscure the parent flow.
- Preserve composability by separating behavior ownership from layout ownership. A dropdown action may own its trigger, open state, and menu content; the caller owns placement such as slots, offsets, and alignment.
+- When a dialog, dropdown, or popover component already accepts controlled `open` state, mount the surface unconditionally unless unmounting is required for performance or reset semantics. Use keyed scope or local state reset for reset behavior instead of `{open && }` wrappers.
- Avoid unnecessary DOM hierarchy. Do not add wrapper elements unless they provide layout, semantics, accessibility, state ownership, or integration with a library API; prefer fragments or styling an existing element when possible.
- Avoid shallow wrappers, hook-to-props adapter components, layout-only render-prop wrappers, children-as-pass-through composition, and prop renaming unless the wrapper adds validation, orchestration, error handling, state ownership, or a real semantic boundary. If a component only calls a hook, forwards props, or passes trigger/content through to one child, move the logic into that child or make the wrapper own a real surface.
@@ -120,6 +135,7 @@ Use this as the decision guide for React/TypeScript component structure. Existin
- Do not use Effects to handle user actions. Put action-specific logic in the event handler where the cause is known.
- Do not use Effects to copy one state value into another state value representing the same concept. Pick one source of truth and derive the rest during render.
- Do not reset or adjust state from props with an Effect. Prefer a `key` reset, storing a stable ID and deriving the selected object, or guarded same-component render-time adjustment when truly necessary.
+- For forms initialized from query data, prefer keyed remounts or surface-entry hydration of form/field atoms over an Effect that copies query data into form state.
- Prefer framework data APIs or TanStack Query for data fetching instead of writing request Effects in components.
- If an Effect still seems necessary, first name the external system it synchronizes with. If there is no external system, remove the Effect and restructure the state or event flow.
diff --git a/web/app/(commonLayout)/deployments/layout.tsx b/web/app/(commonLayout)/deployments/layout.tsx
index eb522444778..b8088169fd5 100644
--- a/web/app/(commonLayout)/deployments/layout.tsx
+++ b/web/app/(commonLayout)/deployments/layout.tsx
@@ -1,11 +1,13 @@
import type { ReactNode } from 'react'
import { DeployDrawer } from '@/features/deployments/deploy-drawer'
+import { DeploymentsRouteStateHydrator } from '@/features/deployments/route-state-hydrator'
export default function DeploymentsLayout({ children }: {
children: ReactNode
}) {
return (
<>
+
{children}
>
diff --git a/web/features/deployments/components/deployment-actions/__tests__/state.spec.ts b/web/features/deployments/components/deployment-actions/__tests__/state.spec.ts
new file mode 100644
index 00000000000..0137a339d1a
--- /dev/null
+++ b/web/features/deployments/components/deployment-actions/__tests__/state.spec.ts
@@ -0,0 +1,222 @@
+import type { Getter } from 'jotai/vanilla'
+import { skipToken } from '@tanstack/react-query'
+import { atom, createStore } from 'jotai'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+type QueryOptions = {
+ enabled?: boolean
+ input?: unknown
+ queryKey?: readonly unknown[]
+}
+
+type QueryResult = {
+ data?: unknown
+}
+
+type MutationOptions = {
+ mutationKey?: readonly string[]
+}
+
+type MutationResult = {
+ isPending: boolean
+ mutate: ReturnType
+ mutateAsync: ReturnType
+}
+
+const mockQueryResults = vi.hoisted(() => ({
+ current: new Map(),
+}))
+
+const mockUpdateMutation = vi.hoisted<{ current: MutationResult }>(() => ({
+ current: {
+ isPending: false,
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ },
+}))
+
+const mockDeleteMutation = vi.hoisted<{ current: MutationResult }>(() => ({
+ current: {
+ isPending: false,
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ },
+}))
+
+vi.mock('jotai-tanstack-query', () => ({
+ atomWithQuery: (createOptions: (get: Getter) => QueryOptions) => atom((get) => {
+ const options = createOptions(get)
+ const queryName = String(options.queryKey?.[0] ?? 'unknown')
+ const queryResult = options.enabled === false
+ ? undefined
+ : mockQueryResults.current.get(queryName)
+
+ return {
+ ...options,
+ data: queryResult?.data,
+ isError: false,
+ isFetching: false,
+ isLoading: false,
+ isSuccess: Boolean(queryResult?.data),
+ }
+ }),
+ atomWithMutation: (createOptions: () => MutationOptions) => atom(() => {
+ const options = createOptions()
+ return options.mutationKey?.[0] === 'deleteAppInstance'
+ ? mockDeleteMutation.current
+ : mockUpdateMutation.current
+ }),
+}))
+
+vi.mock('@/service/client', () => ({
+ consoleQuery: {
+ enterprise: {
+ appInstanceService: {
+ getAppInstance: {
+ queryOptions: (options: QueryOptions) => ({
+ ...options,
+ queryKey: ['getAppInstance', options.input],
+ }),
+ },
+ updateAppInstance: {
+ mutationOptions: () => ({ mutationKey: ['updateAppInstance'] }),
+ },
+ deleteAppInstance: {
+ mutationOptions: () => ({ mutationKey: ['deleteAppInstance'] }),
+ },
+ },
+ },
+ },
+}))
+
+async function loadState() {
+ return await import('../state')
+}
+
+async function mountedStore() {
+ const state = await loadState()
+ const store = createStore()
+ const unsubscribe = store.sub(state.editDeploymentFormCanSaveAtom, () => undefined)
+
+ store.set(state.deploymentActionAppInstanceIdHydrationAtom, 'app-instance-1')
+
+ return {
+ state,
+ store,
+ unsubscribe,
+ }
+}
+
+function setAppInstance(overrides: Record = {}) {
+ mockQueryResults.current.set('getAppInstance', {
+ data: {
+ appInstance: {
+ id: 'app-instance-1',
+ displayName: 'Deployment 1',
+ description: 'Initial description',
+ ...overrides,
+ },
+ },
+ })
+}
+
+describe('deployment action state', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockQueryResults.current.clear()
+ mockUpdateMutation.current = {
+ isPending: false,
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ }
+ mockDeleteMutation.current = {
+ isPending: false,
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ }
+ })
+
+ it('should fetch app instance data only while an action dialog is open', async () => {
+ const { state, store, unsubscribe } = await mountedStore()
+
+ expect(store.get(state.deploymentActionAppInstanceQueryAtom)).toMatchObject({
+ enabled: false,
+ input: skipToken,
+ })
+
+ store.set(state.editDeploymentDialogOpenAtom, true)
+ expect(store.get(state.deploymentActionAppInstanceQueryAtom)).toMatchObject({
+ enabled: true,
+ input: { params: { appInstanceId: 'app-instance-1' } },
+ })
+
+ store.set(state.editDeploymentDialogOpenAtom, false)
+ store.set(state.deleteDeploymentDialogOpenAtom, true)
+ expect(store.get(state.deploymentActionAppInstanceQueryAtom)).toMatchObject({
+ enabled: true,
+ input: { params: { appInstanceId: 'app-instance-1' } },
+ })
+
+ unsubscribe()
+ })
+
+ it('should keep an edit dialog open while update is pending', async () => {
+ const { state, store, unsubscribe } = await mountedStore()
+ mockUpdateMutation.current = {
+ isPending: true,
+ mutate: vi.fn(),
+ mutateAsync: vi.fn(),
+ }
+ store.set(state.editDeploymentDialogOpenAtom, true)
+
+ store.set(state.setEditDeploymentDialogOpenAtom, false)
+
+ expect(store.get(state.editDeploymentDialogOpenAtom)).toBe(true)
+
+ unsubscribe()
+ })
+
+ it('should submit edited deployment metadata with trimmed values', async () => {
+ const { state, store, unsubscribe } = await mountedStore()
+ const response = { appInstance: { id: 'app-instance-1' } }
+ setAppInstance()
+ mockUpdateMutation.current.mutateAsync.mockResolvedValue(response)
+ store.set(state.editDeploymentDialogOpenAtom, true)
+ store.set(state.editDeploymentNameFieldAtom, ' Deployment 2 ')
+ store.set(state.editDeploymentDescriptionFieldAtom, ' Updated description ')
+
+ const result = await store.set(state.submitEditDeploymentFormAtom)
+
+ expect(result).toBe(true)
+ expect(mockUpdateMutation.current.mutateAsync).toHaveBeenCalledWith({
+ params: {
+ appInstanceId: 'app-instance-1',
+ },
+ body: {
+ appInstanceId: 'app-instance-1',
+ displayName: 'Deployment 2',
+ description: 'Updated description',
+ },
+ })
+
+ unsubscribe()
+ })
+
+ it('should submit delete with the hydrated app instance id and caller callbacks', async () => {
+ const { state, store, unsubscribe } = await mountedStore()
+ const onSuccess = vi.fn()
+
+ store.set(state.submitDeleteDeploymentInstanceAtom, { onSuccess })
+
+ expect(mockDeleteMutation.current.mutate).toHaveBeenCalledWith(
+ {
+ params: {
+ appInstanceId: 'app-instance-1',
+ },
+ },
+ { onSuccess },
+ )
+
+ unsubscribe()
+ })
+})
diff --git a/web/features/deployments/components/deployment-actions/delete-dialog.tsx b/web/features/deployments/components/deployment-actions/delete-dialog.tsx
index e2a79e060b6..6af38106365 100644
--- a/web/features/deployments/components/deployment-actions/delete-dialog.tsx
+++ b/web/features/deployments/components/deployment-actions/delete-dialog.tsx
@@ -10,47 +10,37 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { toast } from '@langgenius/dify-ui/toast'
-import { useMutation } from '@tanstack/react-query'
+import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { useTranslation } from 'react-i18next'
import { useRouter } from '@/next/navigation'
-import { consoleQuery } from '@/service/client'
+import {
+ deleteDeploymentDialogOpenAtom,
+ deleteDeploymentInstanceMutationAtom,
+ deploymentActionDisplayNameAtom,
+ submitDeleteDeploymentInstanceAtom,
+} from './state'
-export function DeleteDeploymentDialog({
- appInstanceId,
- appName,
- open,
- onOpenChange,
-}: {
- appInstanceId: string
- appName?: string
- open: boolean
- onOpenChange: (open: boolean) => void
-}) {
+export function DeleteDeploymentDialog() {
const { t } = useTranslation('deployments')
const router = useRouter()
- const deleteInstance = useMutation(consoleQuery.enterprise.appInstanceService.deleteAppInstance.mutationOptions())
- const displayName = appName || appInstanceId
+ const [open, setOpen] = useAtom(deleteDeploymentDialogOpenAtom)
+ const deleteInstance = useAtomValue(deleteDeploymentInstanceMutationAtom)
+ const submitDeleteInstance = useSetAtom(submitDeleteDeploymentInstanceAtom)
+ const displayName = useAtomValue(deploymentActionDisplayNameAtom)
function handleDelete() {
- deleteInstance.mutate(
- {
- params: {
- appInstanceId,
- },
+ submitDeleteInstance({
+ onSuccess: () => {
+ toast.success(t('settings.deleted'))
+ router.push('/deployments')
},
- {
- onSuccess: () => {
- toast.success(t('settings.deleted'))
- router.push('/deployments')
- },
- onError: () => {
- toast.error(t('settings.deleteFailed'))
- },
- onSettled: () => {
- onOpenChange(false)
- },
+ onError: () => {
+ toast.error(t('settings.deleteFailed'))
},
- )
+ onSettled: () => {
+ setOpen(false)
+ },
+ })
}
return (
@@ -59,7 +49,7 @@ export function DeleteDeploymentDialog({
onOpenChange={(nextOpen) => {
if (!nextOpen && deleteInstance.isPending)
return
- onOpenChange(nextOpen)
+ setOpen(nextOpen)
}}
>
diff --git a/web/features/deployments/components/deployment-actions/edit-dialog.tsx b/web/features/deployments/components/deployment-actions/edit-dialog.tsx
index a1a367022a1..b433771b4e5 100644
--- a/web/features/deployments/components/deployment-actions/edit-dialog.tsx
+++ b/web/features/deployments/components/deployment-actions/edit-dialog.tsx
@@ -1,6 +1,5 @@
'use client'
-import type { AppInstance } from '@dify/contracts/enterprise/types.gen'
import type { FormEvent } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import {
@@ -12,16 +11,22 @@ import {
import { Input } from '@langgenius/dify-ui/input'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
-import { useMutation, useQuery } from '@tanstack/react-query'
-import { useState } from 'react'
+import { useAtom, useAtomValue, useSetAtom } from 'jotai'
+import { ScopeProvider } from 'jotai-scope'
import { useTranslation } from 'react-i18next'
import { SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
-import { consoleQuery } from '@/service/client'
-
-type EditDeploymentFormValues = {
- name: string
- description: string
-}
+import {
+ deploymentActionAppInstanceQueryAtom,
+ editDeploymentDescriptionFieldAtom,
+ editDeploymentDialogOpenAtom,
+ editDeploymentFormAtom,
+ editDeploymentFormCanSaveAtom,
+ editDeploymentFormSavePendingAtom,
+ editDeploymentNameFieldAtom,
+ setEditDeploymentDialogOpenAtom,
+ submitEditDeploymentFormAtom,
+ updateDeploymentInstanceMutationAtom,
+} from './state'
function EditDeploymentFormSkeleton() {
return (
@@ -42,35 +47,34 @@ function EditDeploymentFormSkeleton() {
)
}
-function EditDeploymentForm({
- app,
- isSaving,
- onClose,
- onSubmit,
-}: {
- app: AppInstance
- isSaving: boolean
- onClose: () => void
- onSubmit: (values: EditDeploymentFormValues) => void
-}) {
+function EditDeploymentForm() {
const { t } = useTranslation('deployments')
- const initialName = app.displayName
- const initialDescription = app.description
- const [name, setName] = useState(initialName)
- const [description, setDescription] = useState(initialDescription)
- const normalizedName = name.trim()
- const normalizedDescription = description.trim()
- const canSave = Boolean(normalizedName && (normalizedName !== initialName || normalizedDescription !== initialDescription) && !isSaving)
+ const [nameField, setNameField] = useAtom(editDeploymentNameFieldAtom)
+ const [descriptionField, setDescriptionField] = useAtom(editDeploymentDescriptionFieldAtom)
+ const canSave = useAtomValue(editDeploymentFormCanSaveAtom)
+ const savePending = useAtomValue(editDeploymentFormSavePendingAtom)
+ const submitEditDeploymentForm = useSetAtom(submitEditDeploymentFormAtom)
+ const requestOpenChange = useSetAtom(setEditDeploymentDialogOpenAtom)
+ const setOpen = useSetAtom(editDeploymentDialogOpenAtom)
- function handleSubmit(event: FormEvent) {
+ async function handleSubmit(event: FormEvent) {
event.preventDefault()
+ event.stopPropagation()
+
if (!canSave)
return
- onSubmit({
- name: normalizedName,
- description: normalizedDescription,
- })
+ try {
+ const didSubmit = await submitEditDeploymentForm()
+ if (!didSubmit)
+ return
+
+ toast.success(t('settings.updated'))
+ setOpen(false)
+ }
+ catch {
+ toast.error(t('settings.updateFailed'))
+ }
}
return (
@@ -81,9 +85,10 @@ function EditDeploymentForm({
setName(event.target.value)}
+ value={nameField.value}
+ onChange={event => setNameField(event.target.value)}
className="h-8"
/>
@@ -93,8 +98,9 @@ function EditDeploymentForm({