mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 04:36:31 +08:00
tweaks
This commit is contained in:
parent
de7795aa80
commit
2cda3e4181
@ -6,7 +6,7 @@ import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitl
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
@ -53,8 +53,6 @@ function AppPicker({ apps, isLoading, value, onChange }: AppPickerProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const triggerRef = useRef<HTMLButtonElement>(null)
|
||||
const [triggerWidth, setTriggerWidth] = useState<number | undefined>(undefined)
|
||||
|
||||
const q = keywords.trim().toLowerCase()
|
||||
const filtered = q
|
||||
@ -62,8 +60,6 @@ function AppPicker({ apps, isLoading, value, onChange }: AppPickerProps) {
|
||||
: apps
|
||||
|
||||
const handleOpenChange = (next: boolean) => {
|
||||
if (next && triggerRef.current)
|
||||
setTriggerWidth(triggerRef.current.offsetWidth)
|
||||
if (!next)
|
||||
setKeywords('')
|
||||
setOpen(next)
|
||||
@ -96,7 +92,6 @@ function AppPicker({ apps, isLoading, value, onChange }: AppPickerProps) {
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex h-10 w-full items-center justify-between rounded-lg border-[0.5px] bg-components-input-bg-normal pr-2 pl-2 text-left transition-colors',
|
||||
@ -142,8 +137,9 @@ function AppPicker({ apps, isLoading, value, onChange }: AppPickerProps) {
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="p-0 overflow-hidden"
|
||||
popupProps={{ style: { width: 'var(--anchor-width, auto)' } }}
|
||||
>
|
||||
<div style={triggerWidth ? { width: triggerWidth } : undefined} className="flex flex-col">
|
||||
<div className="flex flex-col">
|
||||
<div className="p-2">
|
||||
<Input
|
||||
showLeftIcon
|
||||
@ -230,16 +226,20 @@ function CreateInstanceForm({ onClose }: {
|
||||
const apps = (appList?.data ?? []).map(toStudioSourceAppInfo)
|
||||
|
||||
const [appId, setAppId] = useState<string>('')
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
|
||||
const selectedApp = apps.find(a => a.id === appId)
|
||||
const canCreate = Boolean(appId && name.trim() && !createInstance.isPending)
|
||||
const canCreate = Boolean(appId && !createInstance.isPending)
|
||||
|
||||
const handleCreate = async () => {
|
||||
const handleCreate = async (form: HTMLFormElement) => {
|
||||
if (!canCreate)
|
||||
return
|
||||
|
||||
const formData = new FormData(form)
|
||||
const name = String(formData.get('name') ?? '').trim()
|
||||
const description = String(formData.get('description') ?? '').trim()
|
||||
if (!name)
|
||||
return
|
||||
|
||||
try {
|
||||
const result = await createInstance.mutateAsync({
|
||||
body: {
|
||||
@ -259,7 +259,13 @@ function CreateInstanceForm({ onClose }: {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<form
|
||||
className="flex flex-col gap-5"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
void handleCreate(event.currentTarget)
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<DialogTitle className="title-xl-semi-bold text-text-primary">
|
||||
{t('createModal.title')}
|
||||
@ -285,10 +291,10 @@ function CreateInstanceForm({ onClose }: {
|
||||
</label>
|
||||
<input
|
||||
id="instance-name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={name}
|
||||
placeholder={selectedApp?.name ?? t('createModal.namePlaceholder')}
|
||||
onChange={e => setName(e.target.value)}
|
||||
required
|
||||
className="flex h-8 items-center rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-3 text-[13px] font-medium text-text-secondary outline-hidden placeholder:text-text-quaternary"
|
||||
/>
|
||||
</div>
|
||||
@ -299,9 +305,8 @@ function CreateInstanceForm({ onClose }: {
|
||||
</label>
|
||||
<textarea
|
||||
id="instance-desc"
|
||||
value={description}
|
||||
name="description"
|
||||
placeholder={t('createModal.descriptionPlaceholder')}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
className="min-h-[80px] rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-3 py-2 text-[13px] text-text-secondary outline-hidden placeholder:text-text-quaternary"
|
||||
/>
|
||||
</div>
|
||||
@ -310,11 +315,11 @@ function CreateInstanceForm({ onClose }: {
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{t('createModal.cancel')}
|
||||
</Button>
|
||||
<Button variant="primary" disabled={!canCreate} onClick={() => void handleCreate()}>
|
||||
<Button type="submit" variant="primary" disabled={!canCreate}>
|
||||
{t('createModal.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -27,18 +27,98 @@ type SettingsFormProps = {
|
||||
onDelete: () => Promise<void>
|
||||
}
|
||||
|
||||
type DeleteInstanceControlProps = {
|
||||
appName: string
|
||||
settings?: GetAppInstanceSettingsReply
|
||||
hasDeployments: boolean
|
||||
onDelete: () => Promise<void>
|
||||
}
|
||||
|
||||
function DeleteInstanceControl({
|
||||
appName,
|
||||
settings,
|
||||
hasDeployments,
|
||||
onDelete,
|
||||
}: DeleteInstanceControlProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const canDelete = !hasDeployments && Boolean(settings) && settings?.deleteGuard?.canDelete !== false
|
||||
|
||||
const handleDelete = () => {
|
||||
void (async () => {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await onDelete()
|
||||
toast.success(t('settings.deleted'))
|
||||
}
|
||||
catch {
|
||||
toast.error(t('settings.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
setIsDeleting(false)
|
||||
setShowDeleteConfirm(false)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-util-colors-red-red-200 bg-util-colors-red-red-50 p-4">
|
||||
<div className="system-sm-semibold text-util-colors-red-red-700">{t('settings.danger')}</div>
|
||||
<div className="system-xs-regular text-util-colors-red-red-600">
|
||||
{t('settings.dangerDesc')}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{hasDeployments
|
||||
? t('settings.undeployFirst')
|
||||
: settings?.deleteGuard?.disabledReason || t('settings.safeToDelete')}
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="destructive"
|
||||
disabled={!canDelete || isDeleting}
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
>
|
||||
{t('settings.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={open => !open && setShowDeleteConfirm(false)}>
|
||||
<AlertDialogContent className="w-[520px]">
|
||||
<div className="flex flex-col gap-3 px-6 pt-6 pb-2">
|
||||
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t('settings.deleteConfirmTitle')}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="system-md-regular text-text-tertiary">
|
||||
{t('settings.deleteConfirmDesc', { name: appName })}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton variant="secondary">
|
||||
{t('createModal.cancel')}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton onClick={handleDelete}>
|
||||
{t('settings.delete')}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsForm({ app, settings, hasDeployments, onSave, onDelete }: SettingsFormProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appName = app.name ?? app.id ?? ''
|
||||
const [name, setName] = useState(settings?.name ?? appName)
|
||||
const [description, setDescription] = useState(settings?.description ?? app.description ?? '')
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const initialName = settings?.name ?? appName
|
||||
const initialDescription = settings?.description ?? app.description ?? ''
|
||||
const canSave = Boolean(name.trim() && (name !== initialName || description !== initialDescription) && !isSaving)
|
||||
const canDelete = !hasDeployments && Boolean(settings) && settings?.deleteGuard?.canDelete !== false
|
||||
|
||||
const handleSave = () => {
|
||||
if (!canSave)
|
||||
@ -61,23 +141,6 @@ function SettingsForm({ app, settings, hasDeployments, onSave, onDelete }: Setti
|
||||
})()
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
void (async () => {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await onDelete()
|
||||
toast.success(t('settings.deleted'))
|
||||
}
|
||||
catch {
|
||||
toast.error(t('settings.deleteFailed'))
|
||||
}
|
||||
finally {
|
||||
setIsDeleting(false)
|
||||
setShowDeleteConfirm(false)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex max-w-[640px] flex-col gap-5 p-6">
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-components-panel-border bg-components-panel-bg p-4">
|
||||
@ -123,47 +186,12 @@ function SettingsForm({ app, settings, hasDeployments, onSave, onDelete }: Setti
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-util-colors-red-red-200 bg-util-colors-red-red-50 p-4">
|
||||
<div className="system-sm-semibold text-util-colors-red-red-700">{t('settings.danger')}</div>
|
||||
<div className="system-xs-regular text-util-colors-red-red-600">
|
||||
{t('settings.dangerDesc')}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{hasDeployments
|
||||
? t('settings.undeployFirst')
|
||||
: settings?.deleteGuard?.disabledReason || t('settings.safeToDelete')}
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="destructive"
|
||||
disabled={!canDelete || isDeleting}
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
>
|
||||
{t('settings.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={open => !open && setShowDeleteConfirm(false)}>
|
||||
<AlertDialogContent className="w-[520px]">
|
||||
<div className="flex flex-col gap-3 px-6 pt-6 pb-2">
|
||||
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t('settings.deleteConfirmTitle')}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="system-md-regular text-text-tertiary">
|
||||
{t('settings.deleteConfirmDesc', { name: appName })}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton variant="secondary">
|
||||
{t('createModal.cancel')}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton onClick={handleDelete}>
|
||||
{t('settings.delete')}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteInstanceControl
|
||||
appName={appName}
|
||||
settings={settings}
|
||||
hasDeployments={hasDeployments}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { DEPLOYMENT_PAGE_SIZE } from '../data'
|
||||
import {
|
||||
@ -30,13 +29,15 @@ function CreateReleaseControl({ appId, canCreateRelease }: {
|
||||
const { t } = useTranslation('deployments')
|
||||
const createRelease = useMutation(consoleQuery.enterprise.appDeploy.createRelease.mutationOptions())
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [releaseName, setReleaseName] = useState('')
|
||||
const [releaseDescription, setReleaseDescription] = useState('')
|
||||
const trimmedReleaseName = releaseName.trim()
|
||||
const canSubmitRelease = Boolean(canCreateRelease && trimmedReleaseName && !createRelease.isPending)
|
||||
|
||||
async function handleCreateRelease() {
|
||||
if (!canSubmitRelease)
|
||||
async function handleCreateRelease(form: HTMLFormElement) {
|
||||
if (!canCreateRelease || createRelease.isPending)
|
||||
return
|
||||
|
||||
const formData = new FormData(form)
|
||||
const releaseName = String(formData.get('name') ?? '').trim()
|
||||
const releaseDescription = String(formData.get('description') ?? '').trim()
|
||||
if (!releaseName)
|
||||
return
|
||||
|
||||
try {
|
||||
@ -45,14 +46,13 @@ function CreateReleaseControl({ appId, canCreateRelease }: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
body: {
|
||||
name: trimmedReleaseName,
|
||||
description: releaseDescription.trim() || undefined,
|
||||
name: releaseName,
|
||||
description: releaseDescription || undefined,
|
||||
},
|
||||
})
|
||||
if (!response.release?.id)
|
||||
throw new Error('Create release did not return a release.')
|
||||
setReleaseName('')
|
||||
setReleaseDescription('')
|
||||
form.reset()
|
||||
setIsCreating(false)
|
||||
}
|
||||
catch {
|
||||
@ -78,7 +78,7 @@ function CreateReleaseControl({ appId, canCreateRelease }: {
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
void handleCreateRelease()
|
||||
void handleCreateRelease(event.currentTarget)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-3 border-b border-divider-subtle px-6 py-5 pr-14">
|
||||
@ -102,10 +102,10 @@ function CreateReleaseControl({ appId, canCreateRelease }: {
|
||||
</label>
|
||||
<Input
|
||||
id="release-name"
|
||||
value={releaseName}
|
||||
onChange={e => setReleaseName(e.target.value)}
|
||||
name="name"
|
||||
placeholder={t('versions.releaseNamePlaceholder')}
|
||||
maxLength={128}
|
||||
required
|
||||
autoFocus
|
||||
className="h-9"
|
||||
/>
|
||||
@ -120,13 +120,12 @@ function CreateReleaseControl({ appId, canCreateRelease }: {
|
||||
{t('versions.optional')}
|
||||
</span>
|
||||
</div>
|
||||
<Textarea
|
||||
<textarea
|
||||
id="release-description"
|
||||
value={releaseDescription}
|
||||
onChange={e => setReleaseDescription(e.target.value)}
|
||||
name="description"
|
||||
placeholder={t('versions.releaseDescriptionPlaceholder')}
|
||||
maxLength={512}
|
||||
className="min-h-[96px] resize-none"
|
||||
className="min-h-[96px] w-full resize-none appearance-none rounded-md border border-transparent bg-components-input-bg-normal p-2 system-sm-regular text-components-input-text-filled caret-primary-600 outline-hidden placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -148,7 +147,7 @@ function CreateReleaseControl({ appId, canCreateRelease }: {
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="min-w-[88px]"
|
||||
disabled={!canSubmitRelease}
|
||||
disabled={!canCreateRelease || createRelease.isPending}
|
||||
>
|
||||
{createRelease.isPending ? t('versions.creating') : t('versions.create')}
|
||||
</Button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user