mirror of
https://github.com/langgenius/dify.git
synced 2026-08-29 03:45:08 +08:00
fix(agent-v2): refine access card interactions (#41254)
This commit is contained in:
+217
-41
@@ -1,5 +1,10 @@
|
||||
import type { AgentAppDetailWithSite } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type {
|
||||
AgentApiAccessResponse,
|
||||
AgentAppDetailWithSite,
|
||||
} from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { AppDetail } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type React from 'react'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
@@ -206,6 +211,42 @@ function createAgent(overrides: Partial<AgentAppDetailWithSite> = {}): AgentAppD
|
||||
}
|
||||
}
|
||||
|
||||
function createAppDetailResponse(overrides: Partial<AppDetail> = {}): AppDetail {
|
||||
return {
|
||||
enable_api: true,
|
||||
enable_site: true,
|
||||
id: 'app-1',
|
||||
mode: 'agent',
|
||||
name: 'Support Agent',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function createAgentApiAccessResponse(
|
||||
overrides: Partial<AgentApiAccessResponse> = {},
|
||||
): AgentApiAccessResponse {
|
||||
const serviceApiBaseUrl = 'https://api.example.test/v1'
|
||||
|
||||
return {
|
||||
access_ready: true,
|
||||
api_key_count: 2,
|
||||
api_rph: 0,
|
||||
api_rpm: 0,
|
||||
chat_endpoint: `${serviceApiBaseUrl}/chat-messages`,
|
||||
conversations_endpoint: `${serviceApiBaseUrl}/conversations`,
|
||||
enabled: true,
|
||||
files_upload_endpoint: `${serviceApiBaseUrl}/files/upload`,
|
||||
info_endpoint: `${serviceApiBaseUrl}/info`,
|
||||
messages_endpoint: `${serviceApiBaseUrl}/messages`,
|
||||
meta_endpoint: `${serviceApiBaseUrl}/meta`,
|
||||
parameters_endpoint: `${serviceApiBaseUrl}/parameters`,
|
||||
service_api_base_url: serviceApiBaseUrl,
|
||||
stop_endpoint: `${serviceApiBaseUrl}/chat-messages/{task_id}/stop`,
|
||||
streaming_only: true,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function renderWithQueryClient(
|
||||
ui: React.ReactElement,
|
||||
{ webAppAuthEnabled = true }: { webAppAuthEnabled?: boolean } = {},
|
||||
@@ -237,19 +278,36 @@ function createConsoleQueryClient(webAppAuthEnabled = true) {
|
||||
return queryClient
|
||||
}
|
||||
|
||||
function createDeferredPromise<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason?: unknown) => void
|
||||
const promise = new Promise<T>((promiseResolve, promiseReject) => {
|
||||
resolve = promiseResolve
|
||||
reject = promiseReject
|
||||
})
|
||||
|
||||
return { promise, reject, resolve }
|
||||
}
|
||||
|
||||
describe('Agent access surface cards', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Web app access', () => {
|
||||
it('should render the backend web app URL and toggle site status through the backing app id', async () => {
|
||||
it('should serialize Web App toggles and cache each confirmed response', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.siteEnableMutation.mockResolvedValueOnce({ enable_site: false })
|
||||
const firstToggle = createDeferredPromise<AppDetail>()
|
||||
const secondToggle = createDeferredPromise<AppDetail>()
|
||||
mocks.siteEnableMutation
|
||||
.mockReturnValueOnce(firstToggle.promise)
|
||||
.mockReturnValueOnce(secondToggle.promise)
|
||||
|
||||
renderWithQueryClient(
|
||||
<WebAppAccessCard agent={createAgent()} agentId="agent-1" isLoading={false} />,
|
||||
const agent = createAgent()
|
||||
const queryClient = renderWithQueryClient(
|
||||
<WebAppAccessCard agent={agent} agentId="agent-1" isLoading={false} />,
|
||||
)
|
||||
queryClient.setQueryData(['agent-detail', 'agent-1'], agent)
|
||||
|
||||
expect(screen.getByText('https://chat.example.test/agent/site-token')).toBeInTheDocument()
|
||||
expect(
|
||||
@@ -257,22 +315,115 @@ describe('Agent access surface cards', () => {
|
||||
).toHaveAttribute('href', 'https://chat.example.test/agent/site-token')
|
||||
expect(screen.getByText('agentV2.agentDetail.access.webApp.ssoEnabled')).toBeInTheDocument()
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('switch', {
|
||||
name: 'agentV2.agentDetail.access.toggleSurface:{"name":"agentV2.agentDetail.access.webApp.title"}',
|
||||
const accessSwitch = screen.getByRole('switch', {
|
||||
name: 'agentV2.agentDetail.access.toggleSurface:{"name":"agentV2.agentDetail.access.webApp.title"}',
|
||||
})
|
||||
await user.click(accessSwitch)
|
||||
|
||||
expect(accessSwitch).toHaveAttribute('aria-checked', 'false')
|
||||
expect(accessSwitch).toBeEnabled()
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'agentV2.agentDetail.access.webApp.actions.launch' }),
|
||||
).toBeDisabled()
|
||||
expect(mocks.siteEnableMutation.mock.calls[0]?.[0]).toEqual({
|
||||
params: {
|
||||
app_id: 'app-1',
|
||||
},
|
||||
body: {
|
||||
enable_site: false,
|
||||
},
|
||||
})
|
||||
|
||||
await user.click(accessSwitch)
|
||||
|
||||
expect(accessSwitch).toHaveAttribute('aria-checked', 'true')
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'agentV2.agentDetail.access.webApp.actions.launch' }),
|
||||
).toBeDisabled()
|
||||
expect(mocks.siteEnableMutation).toHaveBeenCalledTimes(1)
|
||||
|
||||
firstToggle.resolve(
|
||||
createAppDetailResponse({
|
||||
enable_site: false,
|
||||
updated_at: 1781660200,
|
||||
updated_by: 'user-2',
|
||||
}),
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.siteEnableMutation.mock.calls[0]?.[0]).toEqual({
|
||||
expect(mocks.siteEnableMutation.mock.calls[1]?.[0]).toEqual({
|
||||
params: {
|
||||
app_id: 'app-1',
|
||||
},
|
||||
body: {
|
||||
enable_site: false,
|
||||
enable_site: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
expect(queryClient.getQueryData(['agent-detail', 'agent-1'])).toMatchObject({
|
||||
enable_site: false,
|
||||
updated_at: 1781660200,
|
||||
updated_by: 'user-2',
|
||||
})
|
||||
|
||||
secondToggle.resolve(
|
||||
createAppDetailResponse({
|
||||
enable_site: true,
|
||||
updated_at: 1781660300,
|
||||
updated_by: 'user-3',
|
||||
}),
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryClient.getQueryData(['agent-detail', 'agent-1'])).toMatchObject({
|
||||
enable_site: true,
|
||||
updated_at: 1781660300,
|
||||
updated_by: 'user-3',
|
||||
})
|
||||
})
|
||||
expect(
|
||||
await screen.findByRole('link', {
|
||||
name: 'agentV2.agentDetail.access.webApp.actions.launch',
|
||||
}),
|
||||
).toHaveAttribute('href', 'https://chat.example.test/agent/site-token')
|
||||
expect(toast.success).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should keep launch disabled while enabling is pending and roll back after failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
const toggle = createDeferredPromise<AppDetail>()
|
||||
mocks.siteEnableMutation.mockReturnValueOnce(toggle.promise)
|
||||
|
||||
renderWithQueryClient(
|
||||
<WebAppAccessCard
|
||||
agent={createAgent({ enable_site: false })}
|
||||
agentId="agent-1"
|
||||
isLoading={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
const accessSwitch = screen.getByRole('switch', {
|
||||
name: 'agentV2.agentDetail.access.toggleSurface:{"name":"agentV2.agentDetail.access.webApp.title"}',
|
||||
})
|
||||
const launchButton = screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.access.webApp.actions.launch',
|
||||
})
|
||||
await user.click(accessSwitch)
|
||||
|
||||
expect(accessSwitch).toHaveAttribute('aria-checked', 'true')
|
||||
expect(accessSwitch).toBeEnabled()
|
||||
expect(launchButton).toBeDisabled()
|
||||
expect(
|
||||
screen.queryByRole('link', { name: 'agentV2.agentDetail.access.webApp.actions.launch' }),
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
toggle.reject(new Error('request failed'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(accessSwitch).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
expect(launchButton).toBeDisabled()
|
||||
expect(toast.error).toHaveBeenCalledWith('common.actionMsg.modifiedUnsuccessfully')
|
||||
})
|
||||
|
||||
it('should open the customize dialog with the backing app id and API base URL', async () => {
|
||||
@@ -616,24 +767,56 @@ describe('Agent access surface cards', () => {
|
||||
describe('Service API access', () => {
|
||||
it('should render service API data and toggle Agent API status through the generated Agent endpoint', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.apiAccessQueryFn.mockResolvedValueOnce({
|
||||
access_ready: true,
|
||||
api_key_count: 2,
|
||||
enabled: true,
|
||||
service_api_base_url: 'https://api.example.test/v1',
|
||||
})
|
||||
mocks.apiEnableMutation.mockResolvedValueOnce({
|
||||
access_ready: true,
|
||||
api_key_count: 2,
|
||||
enabled: false,
|
||||
service_api_base_url: 'https://api.example.test/v1',
|
||||
})
|
||||
const toggle = createDeferredPromise<AgentApiAccessResponse>()
|
||||
mocks.apiAccessQueryFn.mockResolvedValueOnce(createAgentApiAccessResponse())
|
||||
mocks.apiEnableMutation.mockReturnValueOnce(toggle.promise)
|
||||
|
||||
renderWithQueryClient(<ServiceApiAccessCard agentId="agent-1" />)
|
||||
|
||||
expect(await screen.findByText('https://api.example.test/v1')).toBeInTheDocument()
|
||||
expect(screen.getByText('2')).toBeInTheDocument()
|
||||
|
||||
const accessSwitch = screen.getByRole('switch', {
|
||||
name: 'agentV2.agentDetail.access.toggleSurface:{"name":"agentV2.agentDetail.access.serviceApi.title"}',
|
||||
})
|
||||
const apiKeyButton = screen.getByRole('button', {
|
||||
name: /agentV2\.agentDetail\.access\.serviceApi\.actions\.apiKey/,
|
||||
})
|
||||
await user.click(accessSwitch)
|
||||
|
||||
expect(accessSwitch).toHaveAttribute('aria-checked', 'false')
|
||||
expect(accessSwitch).toBeEnabled()
|
||||
expect(apiKeyButton).toBeEnabled()
|
||||
expect(mocks.apiEnableMutation.mock.calls[0]?.[0]).toEqual({
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
body: {
|
||||
enable_api: false,
|
||||
},
|
||||
})
|
||||
|
||||
toggle.reject(new Error('request failed'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(accessSwitch).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
expect(toast.error).toHaveBeenCalledWith('common.actionMsg.modifiedUnsuccessfully')
|
||||
expect(toast.success).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should keep raw API enablement separate from effective API access status', async () => {
|
||||
const user = userEvent.setup()
|
||||
const initialApiAccess = createAgentApiAccessResponse({ enabled: false })
|
||||
const updatedApiAccess = createAgentApiAccessResponse({ access_ready: false, enabled: false })
|
||||
mocks.apiAccessQueryFn.mockResolvedValueOnce(initialApiAccess)
|
||||
mocks.apiEnableMutation.mockResolvedValueOnce(updatedApiAccess)
|
||||
|
||||
const agent = createAgent({ enable_api: false })
|
||||
const queryClient = renderWithQueryClient(<ServiceApiAccessCard agentId="agent-1" />)
|
||||
queryClient.setQueryData(['agent-detail', 'agent-1'], agent)
|
||||
|
||||
await screen.findByText(initialApiAccess.service_api_base_url)
|
||||
await user.click(
|
||||
screen.getByRole('switch', {
|
||||
name: 'agentV2.agentDetail.access.toggleSurface:{"name":"agentV2.agentDetail.access.serviceApi.title"}',
|
||||
@@ -641,25 +824,17 @@ describe('Agent access surface cards', () => {
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.apiEnableMutation.mock.calls[0]?.[0]).toEqual({
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
body: {
|
||||
enable_api: false,
|
||||
},
|
||||
expect(queryClient.getQueryData(['agent-api-access', 'agent-1'])).toEqual(updatedApiAccess)
|
||||
expect(queryClient.getQueryData(['agent-detail', 'agent-1'])).toMatchObject({
|
||||
enable_api: true,
|
||||
})
|
||||
})
|
||||
expect(toast.success).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should manage API keys with the Agent API key endpoints', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.apiAccessQueryFn.mockResolvedValue({
|
||||
access_ready: true,
|
||||
api_key_count: 1,
|
||||
enabled: true,
|
||||
service_api_base_url: 'https://api.example.test/v1',
|
||||
})
|
||||
mocks.apiAccessQueryFn.mockResolvedValue(createAgentApiAccessResponse({ api_key_count: 1 }))
|
||||
mocks.apiKeysQueryFn.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
@@ -721,12 +896,13 @@ describe('Agent access surface cards', () => {
|
||||
|
||||
it('should explain that publishing enables the Service API switch', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.apiAccessQueryFn.mockResolvedValueOnce({
|
||||
access_ready: false,
|
||||
api_key_count: 0,
|
||||
enabled: false,
|
||||
service_api_base_url: 'https://api.example.test/v1',
|
||||
})
|
||||
mocks.apiAccessQueryFn.mockResolvedValueOnce(
|
||||
createAgentApiAccessResponse({
|
||||
access_ready: false,
|
||||
api_key_count: 0,
|
||||
enabled: false,
|
||||
}),
|
||||
)
|
||||
|
||||
renderWithQueryClient(<ServiceApiAccessCard agentId="agent-1" />)
|
||||
|
||||
|
||||
@@ -25,12 +25,8 @@ export type AccessSurfaceCardProps = {
|
||||
endpointActions?: ReactNode
|
||||
disabled?: boolean
|
||||
disabledReason?: string
|
||||
busy?: boolean
|
||||
}
|
||||
|
||||
export const accessSurfaceActionClassName =
|
||||
'inline-flex h-8 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3 text-[13px] leading-4 font-medium text-components-button-secondary-text shadow-xs outline-hidden backdrop-blur-[5px] hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
|
||||
|
||||
export function AccessSurfaceCard({
|
||||
title,
|
||||
icon,
|
||||
@@ -45,7 +41,6 @@ export function AccessSurfaceCard({
|
||||
endpointActions,
|
||||
disabled = false,
|
||||
disabledReason,
|
||||
busy = false,
|
||||
}: AccessSurfaceCardProps) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
@@ -62,13 +57,13 @@ export function AccessSurfaceCard({
|
||||
<Switch
|
||||
size="md"
|
||||
checked={enabled}
|
||||
disabled={busy || (disabled && !hasDisabledReason)}
|
||||
disabled={disabled && !hasDisabledReason}
|
||||
readOnly={hasDisabledReason}
|
||||
aria-disabled={hasDisabledReason || undefined}
|
||||
data-disabled={hasDisabledReason ? '' : undefined}
|
||||
aria-label={t(($) => $['agentDetail.access.toggleSurface'], { name: title })}
|
||||
onCheckedChange={(nextEnabled) => {
|
||||
if (!disabled && !busy) onEnabledChange(nextEnabled)
|
||||
if (!disabled) onEnabledChange(nextEnabled)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import type { AgentAppDetailWithSite } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { accessSurfaceActionClassName, AccessSurfaceCard } from './access-surface-card'
|
||||
import { AccessSurfaceCard } from './access-surface-card'
|
||||
import { AgentApiKeyModal } from './agent-api-key-modal'
|
||||
|
||||
export function ServiceApiAccessCard({ agentId }: { agentId: string }) {
|
||||
@@ -28,6 +28,9 @@ export function ServiceApiAccessCard({ agentId }: { agentId: string }) {
|
||||
const apiAccess = apiAccessQuery.data
|
||||
const toggleServiceApiMutation = useMutation(
|
||||
consoleQuery.agent.byAgentId.apiEnable.post.mutationOptions({
|
||||
scope: {
|
||||
id: `agent-service-api-toggle:${agentId}`,
|
||||
},
|
||||
onSuccess: (updatedApiAccess, variables) => {
|
||||
queryClient.setQueryData(apiAccessQueryOptions.queryKey, updatedApiAccess)
|
||||
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
|
||||
@@ -40,7 +43,6 @@ export function ServiceApiAccessCard({ agentId }: { agentId: string }) {
|
||||
}
|
||||
: agentDetail,
|
||||
)
|
||||
toast.success(tCommon(($) => $['actionMsg.modifiedSuccessfully']))
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(tCommon(($) => $['actionMsg.modifiedUnsuccessfully']))
|
||||
@@ -48,7 +50,11 @@ export function ServiceApiAccessCard({ agentId }: { agentId: string }) {
|
||||
}),
|
||||
)
|
||||
const accessReady = Boolean(apiAccess?.access_ready)
|
||||
const isBusy = apiAccessQuery.isPending || toggleServiceApiMutation.isPending
|
||||
const pendingEnabled = toggleServiceApiMutation.variables?.body.enable_api
|
||||
const optimisticEnabled =
|
||||
toggleServiceApiMutation.isPending && pendingEnabled !== undefined
|
||||
? pendingEnabled
|
||||
: Boolean(apiAccess?.enabled)
|
||||
const showPublishRequiredMessage =
|
||||
!apiAccessQuery.isPending && !apiAccessQuery.isError && !accessReady
|
||||
|
||||
@@ -71,20 +77,18 @@ export function ServiceApiAccessCard({ agentId }: { agentId: string }) {
|
||||
iconClassName="bg-state-accent-solid text-text-primary-on-surface"
|
||||
endpointLabel={t(($) => $['agentDetail.access.serviceApi.endpoint'])}
|
||||
endpoint={apiAccess?.service_api_base_url ?? ''}
|
||||
enabled={Boolean(apiAccess?.enabled)}
|
||||
enabled={optimisticEnabled}
|
||||
onEnabledChange={handleEnabledChange}
|
||||
copyLabel={t(($) => $['agentDetail.access.copyServiceEndpoint'])}
|
||||
disabled={apiAccessQuery.isPending || apiAccessQuery.isError || !accessReady}
|
||||
disabledReason={
|
||||
showPublishRequiredMessage ? t(($) => $['agentDetail.access.publishRequired']) : undefined
|
||||
}
|
||||
busy={toggleServiceApiMutation.isPending}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="px-3"
|
||||
disabled={isBusy || apiAccessQuery.isError || !accessReady}
|
||||
disabled={apiAccessQuery.isPending || apiAccessQuery.isError || !accessReady}
|
||||
onClick={() => setApiKeyModalOpen(true)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-key-2-line size-4" />
|
||||
@@ -98,7 +102,7 @@ export function ServiceApiAccessCard({ agentId }: { agentId: string }) {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={t(($) => $['agentDetail.access.serviceApi.actions.apiReference'])}
|
||||
className={accessSurfaceActionClassName}
|
||||
className={buttonVariants({ variant: 'secondary', size: 'medium' })}
|
||||
>
|
||||
<span aria-hidden className="i-ri-book-open-line size-4" />
|
||||
{t(($) => $['agentDetail.access.serviceApi.actions.apiReference'])}
|
||||
@@ -107,7 +111,6 @@ export function ServiceApiAccessCard({ agentId }: { agentId: string }) {
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="px-3"
|
||||
onClick={() => {
|
||||
void apiAccessQuery.refetch()
|
||||
}}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AgentAppDetailWithSite } from '@dify/contracts/api/console/agent/t
|
||||
import type { AppSiteUpdatePayload } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { ConfigParams, SettingsAppInfo } from '@/app/components/app/overview/settings'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
@@ -17,9 +17,60 @@ import ShareQRCode from '@/app/components/base/qrcode'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { accessSurfaceActionClassName, AccessSurfaceCard } from './access-surface-card'
|
||||
import { AccessSurfaceCard } from './access-surface-card'
|
||||
import { WebAppAccessControlButton } from './web-app-access-control-button'
|
||||
|
||||
function WebAppLaunchAction({
|
||||
href,
|
||||
label,
|
||||
disabledReason,
|
||||
}: {
|
||||
href?: string
|
||||
label: string
|
||||
disabledReason?: string
|
||||
}) {
|
||||
const content = (
|
||||
<>
|
||||
<span aria-hidden className="i-ri-external-link-line size-4" />
|
||||
{label}
|
||||
</>
|
||||
)
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={label}
|
||||
className={buttonVariants({ variant: 'secondary', size: 'medium' })}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
const disabledButton = (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
disabled
|
||||
focusableWhenDisabled={Boolean(disabledReason)}
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
)
|
||||
|
||||
if (!disabledReason) return disabledButton
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={disabledButton} />
|
||||
<TooltipContent role="tooltip">{disabledReason}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export function WebAppAccessCard({
|
||||
agent,
|
||||
agentId,
|
||||
@@ -39,7 +90,6 @@ export function WebAppAccessCard({
|
||||
const appBaseUrl =
|
||||
site?.app_base_url || (typeof window === 'undefined' ? '' : window.location.origin)
|
||||
const webAppUrl = getAgentWebAppUrl(agent)
|
||||
const isEnabled = Boolean(agent?.enable_site)
|
||||
const accessReady = Boolean(agent?.access_ready)
|
||||
const canManageWebApp = Boolean(appId && accessReady)
|
||||
const embeddedConfig =
|
||||
@@ -71,19 +121,22 @@ export function WebAppAccessCard({
|
||||
})
|
||||
const toggleSiteMutation = useMutation(
|
||||
consoleQuery.apps.byAppId.siteEnable.post.mutationOptions({
|
||||
onSuccess: async (_updatedApp, variables) => {
|
||||
scope: {
|
||||
id: `agent-web-app-toggle:${agentId}`,
|
||||
},
|
||||
onSuccess: (updatedApp) => {
|
||||
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
|
||||
agentDetailQueryKey,
|
||||
(agentDetail) =>
|
||||
agentDetail
|
||||
? {
|
||||
...agentDetail,
|
||||
enable_site: variables.body.enable_site,
|
||||
enable_site: updatedApp.enable_site,
|
||||
updated_at: updatedApp.updated_at,
|
||||
updated_by: updatedApp.updated_by,
|
||||
}
|
||||
: agentDetail,
|
||||
)
|
||||
await queryClient.invalidateQueries({ queryKey: agentDetailQueryKey })
|
||||
toast.success(tCommon(($) => $['actionMsg.modifiedSuccessfully']))
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(tCommon(($) => $['actionMsg.modifiedUnsuccessfully']))
|
||||
@@ -116,24 +169,15 @@ export function WebAppAccessCard({
|
||||
}),
|
||||
)
|
||||
const updateSiteMutation = useMutation(consoleQuery.apps.byAppId.site.post.mutationOptions())
|
||||
const isBusy =
|
||||
toggleSiteMutation.isPending ||
|
||||
resetAccessTokenMutation.isPending ||
|
||||
updateSiteMutation.isPending
|
||||
const pendingEnabled = toggleSiteMutation.variables?.body.enable_site
|
||||
const optimisticEnabled =
|
||||
toggleSiteMutation.isPending && pendingEnabled !== undefined
|
||||
? pendingEnabled
|
||||
: Boolean(agent?.enable_site)
|
||||
const launchHref =
|
||||
webAppUrl && agent?.enable_site && !toggleSiteMutation.isPending ? webAppUrl : undefined
|
||||
const publishRequiredMessage = t(($) => $['agentDetail.access.publishRequired'])
|
||||
const showPublishRequiredMessage = !isLoading && !accessReady
|
||||
const disabledLaunchButton = (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="px-3"
|
||||
disabled
|
||||
focusableWhenDisabled={showPublishRequiredMessage}
|
||||
>
|
||||
<span aria-hidden className="i-ri-external-link-line size-4" />
|
||||
{t(($) => $['agentDetail.access.webApp.actions.launch'])}
|
||||
</Button>
|
||||
)
|
||||
|
||||
function handleEnabledChange(enabled: boolean) {
|
||||
if (!appId) return
|
||||
@@ -212,7 +256,7 @@ export function WebAppAccessCard({
|
||||
iconClassName="bg-state-accent-solid text-text-primary-on-surface"
|
||||
endpointLabel={t(($) => $['agentDetail.access.webApp.accessUrl'])}
|
||||
endpoint={webAppUrl}
|
||||
enabled={isEnabled}
|
||||
enabled={optimisticEnabled}
|
||||
onEnabledChange={handleEnabledChange}
|
||||
copyLabel={t(($) => $['agentDetail.access.copyAccessUrl'])}
|
||||
badge={showSsoBadge ? <SsoBadge /> : undefined}
|
||||
@@ -226,7 +270,7 @@ export function WebAppAccessCard({
|
||||
size="small"
|
||||
className="size-6 shrink-0 px-0 text-text-tertiary hover:text-text-secondary"
|
||||
aria-label={t(($) => $['agentDetail.access.webApp.refreshUrl'])}
|
||||
disabled={!canManageWebApp || isBusy}
|
||||
disabled={!canManageWebApp || resetAccessTokenMutation.isPending}
|
||||
onClick={handleRefreshUrl}
|
||||
>
|
||||
<span aria-hidden className="i-ri-refresh-line size-4" />
|
||||
@@ -236,31 +280,15 @@ export function WebAppAccessCard({
|
||||
}
|
||||
disabled={isLoading || !canManageWebApp}
|
||||
disabledReason={showPublishRequiredMessage ? publishRequiredMessage : undefined}
|
||||
busy={isBusy}
|
||||
>
|
||||
{webAppUrl && isEnabled ? (
|
||||
<a
|
||||
href={webAppUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={t(($) => $['agentDetail.access.webApp.actions.launch'])}
|
||||
className={accessSurfaceActionClassName}
|
||||
>
|
||||
<span aria-hidden className="i-ri-external-link-line size-4" />
|
||||
{t(($) => $['agentDetail.access.webApp.actions.launch'])}
|
||||
</a>
|
||||
) : showPublishRequiredMessage ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={disabledLaunchButton} />
|
||||
<TooltipContent role="tooltip">{publishRequiredMessage}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
disabledLaunchButton
|
||||
)}
|
||||
<WebAppLaunchAction
|
||||
href={launchHref}
|
||||
label={t(($) => $['agentDetail.access.webApp.actions.launch'])}
|
||||
disabledReason={showPublishRequiredMessage ? publishRequiredMessage : undefined}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="px-3"
|
||||
disabled={!embeddedConfig}
|
||||
onClick={() => setShowEmbeddedModal(true)}
|
||||
>
|
||||
@@ -270,7 +298,6 @@ export function WebAppAccessCard({
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="px-3"
|
||||
disabled={!customizeConfig}
|
||||
onClick={() => setShowCustomizeModal(true)}
|
||||
>
|
||||
@@ -280,7 +307,6 @@ export function WebAppAccessCard({
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="px-3"
|
||||
disabled={!settingsAppInfo || updateSiteMutation.isPending}
|
||||
onClick={() => setShowSettingsModal(true)}
|
||||
>
|
||||
|
||||
+1
-6
@@ -45,12 +45,7 @@ export function WebAppAccessControlButton({ agent }: { agent?: AgentAppDetailWit
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="px-3"
|
||||
onClick={() => setShowAccessControl(true)}
|
||||
>
|
||||
<Button variant="secondary" size="medium" onClick={() => setShowAccessControl(true)}>
|
||||
<span aria-hidden className="i-ri-lock-2-line size-4" />
|
||||
{t(($) => $['agentDetail.access.webApp.actions.accessControl'])}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user