diff --git a/.agents/skills/how-to-write-component/SKILL.md b/.agents/skills/how-to-write-component/SKILL.md index 5e20cc2d12..8478a4824f 100644 --- a/.agents/skills/how-to-write-component/SKILL.md +++ b/.agents/skills/how-to-write-component/SKILL.md @@ -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 diff --git a/web/features/deployments/detail/access-tab.tsx b/web/features/deployments/detail/access-tab.tsx index bffa42af16..b6249bedc1 100644 --- a/web/features/deployments/detail/access-tab.tsx +++ b/web/features/deployments/detail/access-tab.tsx @@ -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 ( setCreatedApiToken(undefined)} /> ) diff --git a/web/features/deployments/detail/access-tab/api-keys.tsx b/web/features/deployments/detail/access-tab/api-keys.tsx index 2da794db31..6ac84039ab 100644 --- a/web/features/deployments/detail/access-tab/api-keys.tsx +++ b/web/features/deployments/detail/access-tab/api-keys.tsx @@ -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 - 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 }: {