mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
tweaks
This commit is contained in:
parent
80ac0f4ce3
commit
ac56e38a2a
@ -9,26 +9,30 @@ Follow existing project patterns first. Use these rules to resolve unclear compo
|
||||
|
||||
## Component Declaration And Exports
|
||||
|
||||
- Do not use `FC` or `React.FC`; type the function signature directly.
|
||||
- Prefer `function` for top-level components and module helpers. Arrow functions are fine for local callbacks, handlers, and APIs that naturally take lambdas.
|
||||
- Type component signatures directly; do not use `FC` or `React.FC`.
|
||||
- Prefer `function` for top-level components and module helpers. Use arrow functions for local callbacks, handlers, and lambda-style APIs.
|
||||
- Prefer named exports. Use default exports only where the framework requires them, such as Next.js route files.
|
||||
|
||||
## Props And API Types
|
||||
|
||||
- Type simple one-off props inline. Use a named `Props` type only when reused, exported, complex, or clearer.
|
||||
- Prefer API-generated or API-returned types at component boundaries. Keep small UI conversion helpers beside the component that needs them.
|
||||
- Avoid duplicate invariant checks across parent and child. If a lower-level component already handles empty or invalid data, let callers pass raw values through and keep the fallback there.
|
||||
- Use API-generated or API-returned types at component boundaries. Keep small UI conversion helpers beside the component that needs them.
|
||||
- Keep fallback and invariant checks at the lowest component that already handles that state; callers should pass raw values through instead of duplicating checks.
|
||||
- Avoid prop drilling. One pass-through layer is acceptable; two or more forwarding-only layers means ownership is in the wrong place.
|
||||
- Put handlers, mutations, queries, and side effects at the component boundary that actually uses them. For row-level actions, the row or a purpose-built row container should own the action; list/layout components should pass data and stable IDs, not callbacks they do not use.
|
||||
- Keep callbacks in a parent only when the parent genuinely coordinates the workflow, such as form submission, shared selection state, cross-row batch behavior, or navigation after a child action.
|
||||
|
||||
## State Ownership
|
||||
|
||||
- Keep state, query state, handlers, and derived UI data near the component that owns the interaction. Do not lift state unless siblings or parents genuinely coordinate through it.
|
||||
- When local UI state must be shared across siblings, distant children, or feature-local surfaces, use a colocated Jotai `atom` instead of prop drilling or lifting state into a broad parent. Keep atoms feature-scoped and UI-owned; do not use them for server/cache state that belongs in query or API data flow.
|
||||
- Keep local state, query state, handlers, and derived UI data near the interaction owner. Do not lift state unless siblings or parents genuinely coordinate through it.
|
||||
- For local UI state shared across siblings, distant children, or feature-local surfaces, use a colocated Jotai `atom`. Keep atoms feature-scoped and UI-owned; do not use them for server/cache state that belongs in query or API data flow.
|
||||
- Prefer uncontrolled components when DOM-owned state is enough. Expose style customization through CSS variables before adding controlled props only for visual changes.
|
||||
|
||||
## Component Boundaries
|
||||
|
||||
- Keep the component's first-render surface separate from secondary interactive surfaces. For dialogs, dropdown menus, popovers, and similar branches, split from the trigger boundary: extract a small local component that owns the trigger, open state, and overlay/menu content when that branch would obscure the parent UI flow. Do not further split the dialog body, menu body, or form content unless it has its own state, reuse, complexity, or semantic boundary.
|
||||
- Avoid shallow wrappers and unnecessary renaming. Call the original function directly unless the wrapper adds validation, orchestration, error handling, state ownership, or a real semantic boundary.
|
||||
- Separate first-render surfaces from secondary interactive surfaces. For dialogs, dropdowns, popovers, and similar branches, extract a small local component that owns the trigger, open state, and overlay/menu content when that branch obscures the parent flow.
|
||||
- Do not further split dialog bodies, menu bodies, or forms unless they have their own state, reuse, complexity, or semantic boundary.
|
||||
- Avoid shallow wrappers and renaming. Call the original function directly unless the wrapper adds validation, orchestration, error handling, state ownership, or a real semantic boundary.
|
||||
|
||||
## Navigation
|
||||
|
||||
@ -36,8 +40,8 @@ Follow existing project patterns first. Use these rules to resolve unclear compo
|
||||
|
||||
## Effects
|
||||
|
||||
- Treat `useEffect` as a last resort. Before adding or keeping one, first try to delete it by deriving values during render, moving event-driven work into handlers, or replacing persistence, subscription, media-query, timer, and DOM sync cases with existing equivalent hooks/APIs.
|
||||
- Do not use `useEffect` directly in components. If an effect remains genuinely unavoidable after checking for a declarative substitute, encapsulate it in a purpose-built hook so the component consumes a declarative API instead of managing the effect inline.
|
||||
- Treat `useEffect` as a last resort. First try deriving values during render, moving event-driven work into handlers, or using existing hooks/APIs for persistence, subscriptions, media queries, timers, and DOM sync.
|
||||
- Do not use `useEffect` directly in components. If unavoidable, encapsulate it in a purpose-built hook so the component consumes a declarative API.
|
||||
|
||||
## Performance
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import type {
|
||||
} from '@dify/contracts/enterprise/types.gen'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
deployedRows,
|
||||
} from '../utils'
|
||||
@ -47,7 +47,6 @@ function DeveloperApiAccessSection({
|
||||
token: string
|
||||
}>()
|
||||
const generateApiKey = useMutation(consoleQuery.enterprise.appDeploy.createDeveloperApiKey.mutationOptions())
|
||||
const revokeApiKey = useMutation(consoleQuery.enterprise.appDeploy.deleteDeveloperApiKey.mutationOptions())
|
||||
const toggleDeveloperAPI = useMutation(consoleQuery.enterprise.appDeploy.updateDeveloperApi.mutationOptions())
|
||||
|
||||
function createApiKeyLabel(environmentId: string) {
|
||||
@ -79,33 +78,13 @@ function DeveloperApiAccessSection({
|
||||
)
|
||||
}
|
||||
|
||||
function handleRevokeApiKey(apiKeyId: string) {
|
||||
revokeApiKey.mutate({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
apiKeyId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCopyApiKey(apiKeyId: string) {
|
||||
const response = await consoleClient.enterprise.appDeploy.revealDeveloperApiKey({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
apiKeyId,
|
||||
},
|
||||
})
|
||||
if (!response.token)
|
||||
throw new Error('Reveal developer API key did not return a token.')
|
||||
return response.token
|
||||
}
|
||||
|
||||
const visibleCreatedApiToken = createdApiToken?.appId === appId
|
||||
? createdApiToken.token
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<DeveloperApiSection
|
||||
appId={appId}
|
||||
apiEnabled={apiEnabled}
|
||||
apiUrl={apiUrl}
|
||||
environments={environments}
|
||||
@ -116,8 +95,6 @@ function DeveloperApiAccessSection({
|
||||
body: { enabled },
|
||||
})}
|
||||
onGenerate={handleGenerateApiKey}
|
||||
onCopyApiKey={handleCopyApiKey}
|
||||
onRevoke={handleRevokeApiKey}
|
||||
onClearCreatedToken={() => setCreatedApiToken(undefined)}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -9,27 +9,53 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
import { environmentName } from '../../utils'
|
||||
import { useCopyFeedback } from './use-copy-feedback'
|
||||
|
||||
export function ApiKeyRow({ apiKey, onCopy, onRevoke }: {
|
||||
function ApiKeyRow({ appId, apiKey }: {
|
||||
appId: string
|
||||
apiKey: DeveloperApiKeyRow
|
||||
onCopy: (apiKeyId: string) => Promise<string>
|
||||
onRevoke: (apiKeyId: string) => void
|
||||
}) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const { copied, showCopied } = useCopyFeedback()
|
||||
const revokeApiKey = useMutation(consoleQuery.enterprise.appDeploy.deleteDeveloperApiKey.mutationOptions())
|
||||
const displayValue = apiKey.maskedKey || apiKey.id || '—'
|
||||
const environmentLabel = environmentName(apiKey.environment)
|
||||
|
||||
async function revealApiKey(apiKeyId: string) {
|
||||
const response = await consoleClient.enterprise.appDeploy.revealDeveloperApiKey({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
apiKeyId,
|
||||
},
|
||||
})
|
||||
if (!response.token)
|
||||
throw new Error('Reveal developer API key did not return a token.')
|
||||
return response.token
|
||||
}
|
||||
|
||||
function handleRevoke() {
|
||||
if (!apiKey.id)
|
||||
return
|
||||
|
||||
revokeApiKey.mutate({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
apiKeyId: apiKey.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!apiKey.id)
|
||||
return
|
||||
|
||||
try {
|
||||
const token = await onCopy(apiKey.id)
|
||||
const token = await revealApiKey(apiKey.id)
|
||||
await navigator.clipboard.writeText(token)
|
||||
showCopied()
|
||||
toast.success(t('access.copyToast'))
|
||||
@ -61,7 +87,7 @@ export function ApiKeyRow({ apiKey, onCopy, onRevoke }: {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => apiKey.id && onRevoke(apiKey.id)}
|
||||
onClick={handleRevoke}
|
||||
aria-label={t('access.revoke')}
|
||||
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive"
|
||||
>
|
||||
@ -72,6 +98,23 @@ export function ApiKeyRow({ apiKey, onCopy, onRevoke }: {
|
||||
)
|
||||
}
|
||||
|
||||
export function ApiKeyList({ appId, apiKeys }: {
|
||||
appId: string
|
||||
apiKeys: DeveloperApiKeyRow[]
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col divide-y divide-divider-subtle">
|
||||
{apiKeys.map(apiKey => (
|
||||
<ApiKeyRow
|
||||
key={apiKey.id}
|
||||
appId={appId}
|
||||
apiKey={apiKey}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ApiKeyGenerateMenu({ environments, onGenerate }: {
|
||||
environments: ConsoleEnvironment[]
|
||||
onGenerate: (environmentId: string) => void
|
||||
|
||||
@ -3,10 +3,11 @@
|
||||
import type { ConsoleEnvironment, DeveloperApiKeyRow } from '@dify/contracts/enterprise/types.gen'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ApiKeyGenerateMenu, ApiKeyRow } from './api-keys'
|
||||
import { ApiKeyGenerateMenu, ApiKeyList } from './api-keys'
|
||||
import { CopyPill, Section } from './common'
|
||||
|
||||
type DeveloperApiSectionProps = {
|
||||
appId: string
|
||||
apiEnabled: boolean
|
||||
apiUrl?: string
|
||||
environments: ConsoleEnvironment[]
|
||||
@ -14,12 +15,11 @@ type DeveloperApiSectionProps = {
|
||||
createdToken?: string
|
||||
onToggle: (enabled: boolean) => void
|
||||
onGenerate: (environmentId: string) => void
|
||||
onCopyApiKey: (apiKeyId: string) => Promise<string>
|
||||
onRevoke: (apiKeyId: string) => void
|
||||
onClearCreatedToken: () => void
|
||||
}
|
||||
|
||||
export function DeveloperApiSection({
|
||||
appId,
|
||||
apiEnabled,
|
||||
apiUrl,
|
||||
environments,
|
||||
@ -27,8 +27,6 @@ export function DeveloperApiSection({
|
||||
createdToken,
|
||||
onToggle,
|
||||
onGenerate,
|
||||
onCopyApiKey,
|
||||
onRevoke,
|
||||
onClearCreatedToken,
|
||||
}: DeveloperApiSectionProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
@ -102,16 +100,10 @@ export function DeveloperApiSection({
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex flex-col divide-y divide-divider-subtle">
|
||||
{apiKeys.map(apiKey => (
|
||||
<ApiKeyRow
|
||||
key={apiKey.id}
|
||||
apiKey={apiKey}
|
||||
onCopy={onCopyApiKey}
|
||||
onRevoke={onRevoke}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<ApiKeyList
|
||||
appId={appId}
|
||||
apiKeys={apiKeys}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -21,25 +21,25 @@ import { deployedRows } from '../utils'
|
||||
type SettingsFormProps = {
|
||||
app: AppInstanceBasicInfo
|
||||
settings?: GetAppInstanceSettingsReply
|
||||
hasDeployments: boolean
|
||||
onSave: (patch: Pick<AppInstanceBasicInfo, 'name' | 'description'>) => Promise<void>
|
||||
onDelete: () => Promise<void>
|
||||
}
|
||||
|
||||
type DeleteInstanceControlProps = {
|
||||
appId: string
|
||||
appName: string
|
||||
settings?: GetAppInstanceSettingsReply
|
||||
hasDeployments: boolean
|
||||
onDelete: () => Promise<void>
|
||||
}
|
||||
|
||||
function DeleteInstanceControl({
|
||||
appId,
|
||||
appName,
|
||||
settings,
|
||||
hasDeployments,
|
||||
onDelete,
|
||||
}: DeleteInstanceControlProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const router = useRouter()
|
||||
const deleteInstance = useMutation(consoleQuery.enterprise.appDeploy.deleteAppInstance.mutationOptions())
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const canDelete = !hasDeployments && Boolean(settings) && settings?.deleteGuard?.canDelete !== false
|
||||
@ -48,8 +48,13 @@ function DeleteInstanceControl({
|
||||
void (async () => {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await onDelete()
|
||||
await deleteInstance.mutateAsync({
|
||||
params: {
|
||||
appInstanceId: appId,
|
||||
},
|
||||
})
|
||||
toast.success(t('settings.deleted'))
|
||||
router.push('/deployments')
|
||||
}
|
||||
catch {
|
||||
toast.error(t('settings.deleteFailed'))
|
||||
@ -109,7 +114,7 @@ function DeleteInstanceControl({
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsForm({ app, settings, hasDeployments, onSave, onDelete }: SettingsFormProps) {
|
||||
function SettingsForm({ app, settings, onSave }: SettingsFormProps) {
|
||||
const { t } = useTranslation('deployments')
|
||||
const appName = app.name ?? app.id ?? ''
|
||||
const [name, setName] = useState(settings?.name ?? appName)
|
||||
@ -141,56 +146,47 @@ function SettingsForm({ app, settings, hasDeployments, onSave, onDelete }: Setti
|
||||
}
|
||||
|
||||
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">
|
||||
<div className="system-sm-semibold text-text-primary">{t('settings.general')}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{t('settings.descriptionHelp')}</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="system-xs-medium-uppercase text-text-tertiary" htmlFor="settings-name">
|
||||
{t('settings.name')}
|
||||
</label>
|
||||
<input
|
||||
id="settings-name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
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>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="system-xs-medium-uppercase text-text-tertiary" htmlFor="settings-desc">
|
||||
{t('settings.description')}
|
||||
</label>
|
||||
<textarea
|
||||
id="settings-desc"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
className="min-h-[96px] 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>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={isSaving || (name === initialName && description === initialDescription)}
|
||||
onClick={() => {
|
||||
setName(initialName)
|
||||
setDescription(initialDescription)
|
||||
}}
|
||||
>
|
||||
{t('settings.reset')}
|
||||
</Button>
|
||||
<Button variant="primary" disabled={!canSave} onClick={handleSave}>
|
||||
{t('settings.save')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-components-panel-border bg-components-panel-bg p-4">
|
||||
<div className="system-sm-semibold text-text-primary">{t('settings.general')}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{t('settings.descriptionHelp')}</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="system-xs-medium-uppercase text-text-tertiary" htmlFor="settings-name">
|
||||
{t('settings.name')}
|
||||
</label>
|
||||
<input
|
||||
id="settings-name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
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>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="system-xs-medium-uppercase text-text-tertiary" htmlFor="settings-desc">
|
||||
{t('settings.description')}
|
||||
</label>
|
||||
<textarea
|
||||
id="settings-desc"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
className="min-h-[96px] 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>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={isSaving || (name === initialName && description === initialDescription)}
|
||||
onClick={() => {
|
||||
setName(initialName)
|
||||
setDescription(initialDescription)
|
||||
}}
|
||||
>
|
||||
{t('settings.reset')}
|
||||
</Button>
|
||||
<Button variant="primary" disabled={!canSave} onClick={handleSave}>
|
||||
{t('settings.save')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DeleteInstanceControl
|
||||
appName={appName}
|
||||
settings={settings}
|
||||
hasDeployments={hasDeployments}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -198,9 +194,7 @@ function SettingsForm({ app, settings, hasDeployments, onSave, onDelete }: Setti
|
||||
export function SettingsTab({ instanceId }: {
|
||||
instanceId: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const updateInstance = useMutation(consoleQuery.enterprise.appDeploy.updateAppInstance.mutationOptions())
|
||||
const deleteInstance = useMutation(consoleQuery.enterprise.appDeploy.deleteAppInstance.mutationOptions())
|
||||
const appInput = { params: { appInstanceId: instanceId } }
|
||||
const { data: overview } = useQuery(consoleQuery.enterprise.appDeploy.getAppInstanceOverview.queryOptions({
|
||||
input: appInput,
|
||||
@ -221,27 +215,26 @@ export function SettingsTab({ instanceId }: {
|
||||
const formKey = `${app.id}-${settingsQuery.data?.name ?? appName}-${settingsQuery.data?.description ?? app.description ?? ''}`
|
||||
|
||||
return (
|
||||
<SettingsForm
|
||||
key={formKey}
|
||||
app={app}
|
||||
settings={settingsQuery.data}
|
||||
hasDeployments={hasDeployments}
|
||||
onSave={async (patch) => {
|
||||
await updateInstance.mutateAsync({
|
||||
params: {
|
||||
appInstanceId: instanceId,
|
||||
},
|
||||
body: patch,
|
||||
})
|
||||
}}
|
||||
onDelete={async () => {
|
||||
await deleteInstance.mutateAsync({
|
||||
params: {
|
||||
appInstanceId: instanceId,
|
||||
},
|
||||
})
|
||||
router.push('/deployments')
|
||||
}}
|
||||
/>
|
||||
<div className="flex max-w-[640px] flex-col gap-5 p-6">
|
||||
<SettingsForm
|
||||
key={formKey}
|
||||
app={app}
|
||||
settings={settingsQuery.data}
|
||||
onSave={async (patch) => {
|
||||
await updateInstance.mutateAsync({
|
||||
params: {
|
||||
appInstanceId: instanceId,
|
||||
},
|
||||
body: patch,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<DeleteInstanceControl
|
||||
appId={instanceId}
|
||||
appName={appName}
|
||||
settings={settingsQuery.data}
|
||||
hasDeployments={hasDeployments}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user