mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
refactor(overview): read app detail from the route query (#43258)
This commit is contained in:
7 files changed
+213
-182
No files matched your search
+26
-49
@@ -1,18 +1,9 @@
|
||||
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||
import { AppACLPermission } from '@/utils/permission'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import ChartView from '../chart-view'
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
appDetail: {
|
||||
id: 'app-1',
|
||||
mode: 'chat',
|
||||
maintainer: 'maintainer-1',
|
||||
permission_keys: [] as string[],
|
||||
},
|
||||
currentUserId: 'user-1',
|
||||
workspacePermissionKeys: [] as string[],
|
||||
chartRenderSpy: vi.fn(),
|
||||
conversationPeriodSpy: vi.fn(),
|
||||
}))
|
||||
@@ -24,13 +15,6 @@ vi.mock('@/context/workspace-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: <T,>(selector: (state: { appDetail: typeof testState.appDetail }) => T): T =>
|
||||
selector({
|
||||
appDetail: testState.appDetail,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => path,
|
||||
}))
|
||||
@@ -103,40 +87,33 @@ vi.mock('@/context/permission-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
describe('ChartView monitor permission', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
testState.appDetail = {
|
||||
id: 'app-1',
|
||||
mode: 'chat',
|
||||
maintainer: 'maintainer-1',
|
||||
permission_keys: [],
|
||||
}
|
||||
testState.currentUserId = 'user-1'
|
||||
testState.workspacePermissionKeys = []
|
||||
describe('ChartView', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('renders chat charts and the page-provided header action', () => {
|
||||
renderWithConsoleQuery(
|
||||
<ChartView
|
||||
appId="app-1"
|
||||
appMode="chat"
|
||||
headerRight={<button type="button">header action</button>}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'common.appMenus.overview' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'header action' })).toBeInTheDocument()
|
||||
expect(screen.getByText('conversations chart')).toBeInTheDocument()
|
||||
expect(screen.getByText('avg session interactions chart')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Monitoring charts are part of the app monitor permission surface.
|
||||
describe('Permissions', () => {
|
||||
it('should not render monitoring charts when app monitor permission is missing', () => {
|
||||
render(<ChartView appId="app-1" headerRight={<button type="button">header action</button>} />)
|
||||
it('renders completion charts from the explicit app mode', () => {
|
||||
renderWithConsoleQuery(<ChartView appId="app-1" appMode="completion" headerRight={null} />)
|
||||
expect(screen.getByText('avg response time chart')).toBeInTheDocument()
|
||||
expect(screen.queryByText('avg session interactions chart')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: 'common.appMenus.overview' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('header action')).not.toBeInTheDocument()
|
||||
expect(testState.chartRenderSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render monitoring charts when app monitor permission is granted', () => {
|
||||
testState.appDetail.permission_keys = [AppACLPermission.Monitor]
|
||||
|
||||
render(<ChartView appId="app-1" headerRight={<button type="button">header action</button>} />)
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'common.appMenus.overview' })).toBeInTheDocument()
|
||||
expect(screen.getByText('header action')).toBeInTheDocument()
|
||||
expect(screen.getByText('conversations chart')).toBeInTheDocument()
|
||||
expect(testState.chartRenderSpy).toHaveBeenCalledWith('conversations')
|
||||
})
|
||||
it('renders workflow charts from the explicit app mode', () => {
|
||||
renderWithConsoleQuery(<ChartView appId="app-2" appMode="workflow" headerRight={null} />)
|
||||
expect(screen.getByText('workflow messages chart')).toBeInTheDocument()
|
||||
expect(screen.queryByText('conversations chart')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
+132
-9
@@ -1,10 +1,18 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { renderWithAccountProfile as render } from '@/test/console/account-profile'
|
||||
import { QueryErrorResetBoundary } from '@tanstack/react-query'
|
||||
import { act, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Suspense } from 'react'
|
||||
import CommonLayoutError from '@/app/(commonLayout)/error'
|
||||
import ErrorBoundary from '@/app/components/base/error-boundary'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { createAppDetailFixture } from '@/test/fixtures/app'
|
||||
import { AppACLPermission } from '@/utils/permission'
|
||||
import OverviewView from '../view'
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
request: vi.fn<() => Promise<Response>>(),
|
||||
appDetail: {
|
||||
id: 'app-1',
|
||||
mode: 'chat',
|
||||
@@ -15,6 +23,11 @@ const testState = vi.hoisted(() => ({
|
||||
workspacePermissionKeys: [] as string[],
|
||||
}))
|
||||
|
||||
vi.mock('@/service/base', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/base')>()),
|
||||
request: testState.request,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/workspace-state', async () => {
|
||||
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
|
||||
return createWorkspaceStateModuleMock(() => ({
|
||||
@@ -22,13 +35,6 @@ vi.mock('@/context/workspace-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: <T,>(selector: (state: { appDetail: typeof testState.appDetail }) => T): T =>
|
||||
selector({
|
||||
appDetail: testState.appDetail,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/overview/apikey-info-panel', () => ({
|
||||
default: () => <div>api key info panel</div>,
|
||||
}))
|
||||
@@ -54,6 +60,16 @@ vi.mock('@/context/permission-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
const render = (ui: Parameters<typeof renderWithConsoleQuery>[0]) => {
|
||||
const queryClient = createConsoleQueryClient()
|
||||
const detail = createAppDetailFixture({ ...testState.appDetail, mode: 'chat' })
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: detail.id } } }),
|
||||
detail,
|
||||
)
|
||||
return renderWithConsoleQuery(ui, { queryClient })
|
||||
}
|
||||
|
||||
describe('OverviewView monitor permission', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -111,3 +127,110 @@ describe('OverviewView monitor permission', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Overview app identity', () => {
|
||||
it('uses the destination permissions when navigating between cached apps', async () => {
|
||||
testState.appDetail.permission_keys = [AppACLPermission.Monitor, AppACLPermission.TracingConfig]
|
||||
const { queryClient, rerender } = render(<OverviewView appId="app-1" />)
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: 'app-2' } } }),
|
||||
createAppDetailFixture({ id: 'app-2', permission_keys: [] }),
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'tracing' })).toBeInTheDocument()
|
||||
|
||||
rerender(<OverviewView appId="app-2" />)
|
||||
expect(screen.queryByText('api key info panel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'tracing' })).not.toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: 'app-2' } } }),
|
||||
createAppDetailFixture({ id: 'app-2', permission_keys: [AppACLPermission.Monitor] }),
|
||||
)
|
||||
})
|
||||
await screen.findByText(/chart view app-2/)
|
||||
act(() => {
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: 'app-2' } } }),
|
||||
createAppDetailFixture({ id: 'app-2', permission_keys: [] }),
|
||||
)
|
||||
})
|
||||
await waitFor(() => expect(screen.queryByText('api key info panel')).not.toBeInTheDocument())
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the page fallback until required app details resolve', async () => {
|
||||
let resolveRequest: (response: Response) => void = () => {}
|
||||
testState.request.mockReturnValue(
|
||||
new Promise<Response>((resolve) => {
|
||||
resolveRequest = resolve
|
||||
}),
|
||||
)
|
||||
renderWithConsoleQuery(
|
||||
<Suspense fallback={<div>Loading overview</div>}>
|
||||
<OverviewView appId="loading-app" />
|
||||
</Suspense>,
|
||||
)
|
||||
expect(screen.getByText('Loading overview')).toBeInTheDocument()
|
||||
expect(screen.queryByText('api key info panel')).not.toBeInTheDocument()
|
||||
await act(async () => {
|
||||
resolveRequest(
|
||||
Response.json(
|
||||
createAppDetailFixture({
|
||||
id: 'loading-app',
|
||||
permission_keys: [AppACLPermission.Monitor],
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
expect(await screen.findByText(/chart view loading-app/)).toBeInTheDocument()
|
||||
expect(screen.queryByText('Loading overview')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('recovers an initial detail failure through the existing route error action', async () => {
|
||||
const user = userEvent.setup()
|
||||
testState.request.mockRejectedValue(new Error('Detail unavailable'))
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
try {
|
||||
renderWithConsoleQuery(
|
||||
<QueryErrorResetBoundary>
|
||||
<ErrorBoundary
|
||||
fallback={(error, retry) => <CommonLayoutError error={error} retry={retry} />}
|
||||
>
|
||||
<Suspense fallback={<div>Loading overview</div>}>
|
||||
<OverviewView appId="unavailable-app" />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</QueryErrorResetBoundary>,
|
||||
)
|
||||
const retry = await screen.findByRole('button', { name: 'common.errorBoundary.tryAgain' })
|
||||
expect(screen.queryByText('api key info panel')).not.toBeInTheDocument()
|
||||
testState.request.mockResolvedValue(
|
||||
Response.json(
|
||||
createAppDetailFixture({
|
||||
id: 'unavailable-app',
|
||||
permission_keys: [AppACLPermission.Monitor],
|
||||
}),
|
||||
),
|
||||
)
|
||||
await user.click(retry)
|
||||
expect(await screen.findByText(/chart view unavailable-app/)).toBeInTheDocument()
|
||||
} finally {
|
||||
consoleError.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps loaded content visible when a background refresh fails', async () => {
|
||||
testState.appDetail.permission_keys = [AppACLPermission.Monitor]
|
||||
testState.request.mockRejectedValue(new Error('Refresh unavailable'))
|
||||
const { queryClient } = render(<OverviewView appId="app-1" />)
|
||||
await act(async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: 'app-1' } } }),
|
||||
})
|
||||
})
|
||||
expect(screen.getByText(/chart view app-1/)).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'common.errorBoundary.tryAgain' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen'
|
||||
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -24,12 +24,8 @@ import {
|
||||
WorkflowDailyTerminalsChart,
|
||||
WorkflowMessagesChart,
|
||||
} from '@/app/components/app/overview/app-chart'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { getAppACLCapabilities } from '@/utils/permission'
|
||||
import LongTimeRangePicker from './long-time-range-picker'
|
||||
import TimeRangePicker from './time-range-picker'
|
||||
|
||||
@@ -49,10 +45,11 @@ const queryDateFormat = 'YYYY-MM-DD HH:mm'
|
||||
|
||||
type IChartViewProps = {
|
||||
appId: string
|
||||
appMode: AppDetailWithSite['mode']
|
||||
headerRight: React.ReactNode
|
||||
}
|
||||
|
||||
export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
||||
export default function ChartView({ appId, appMode, headerRight }: IChartViewProps) {
|
||||
const { data: deploymentEdition } = useSuspenseQuery({
|
||||
...systemFeaturesQueryOptions(),
|
||||
select: ({ deployment_edition }) => deployment_edition,
|
||||
@@ -61,6 +58,7 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
||||
return (
|
||||
<ChartViewContent
|
||||
appId={appId}
|
||||
appMode={appMode}
|
||||
headerRight={headerRight}
|
||||
deploymentEdition={deploymentEdition}
|
||||
/>
|
||||
@@ -69,6 +67,7 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
|
||||
|
||||
function ChartViewContent({
|
||||
appId,
|
||||
appMode,
|
||||
headerRight,
|
||||
deploymentEdition,
|
||||
}: IChartViewProps & { deploymentEdition: DeploymentEdition }) {
|
||||
@@ -76,23 +75,8 @@ function ChartViewContent({
|
||||
const isCloudEdition = deploymentEdition === 'CLOUD'
|
||||
const isNonCloudEdition = deploymentEdition === 'COMMUNITY' || deploymentEdition === 'ENTERPRISE'
|
||||
const docLink = useDocLink()
|
||||
const appDetail = useAppStore((state) => state.appDetail)
|
||||
const { data: currentUserId } = useSuspenseQuery({
|
||||
...userProfileQueryOptions(),
|
||||
select: (data) => data.profile.id,
|
||||
})
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const canMonitor = React.useMemo(
|
||||
() =>
|
||||
getAppACLCapabilities(appDetail?.permission_keys, {
|
||||
currentUserId,
|
||||
resourceMaintainer: appDetail?.maintainer,
|
||||
workspacePermissionKeys,
|
||||
}).canMonitor,
|
||||
[appDetail?.maintainer, appDetail?.permission_keys, currentUserId, workspacePermissionKeys],
|
||||
)
|
||||
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
|
||||
const isWorkflow = appDetail?.mode === 'workflow'
|
||||
const isChatApp = appMode !== 'completion' && appMode !== 'workflow'
|
||||
const isWorkflow = appMode === 'workflow'
|
||||
const [period, setPeriod] = useState<PeriodParams>(() =>
|
||||
isCloudEdition
|
||||
? {
|
||||
@@ -111,8 +95,6 @@ function ChartViewContent({
|
||||
},
|
||||
)
|
||||
|
||||
if (!appDetail || !canMonitor) return null
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="h-26.5 shrink-0">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Suspense } from 'react'
|
||||
import * as React from 'react'
|
||||
import { SkeletonContainer, SkeletonRectangle } from '@/app/components/base/skeleton'
|
||||
import OverviewView from './view'
|
||||
|
||||
export type IDevelopProps = {
|
||||
@@ -10,7 +12,19 @@ const Overview = async (props: IDevelopProps) => {
|
||||
|
||||
const { appId } = params
|
||||
|
||||
return <OverviewView appId={appId} />
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<SkeletonContainer className="gap-4 p-6" aria-busy="true">
|
||||
<SkeletonRectangle className="h-6 w-48" />
|
||||
<SkeletonRectangle className="h-10 w-full" />
|
||||
<SkeletonRectangle className="h-96 w-full" />
|
||||
</SkeletonContainer>
|
||||
}
|
||||
>
|
||||
<OverviewView appId={appId} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export default Overview
|
||||
+7
-58
@@ -1,40 +1,15 @@
|
||||
import type { ComponentProps, ReactNode } from 'react'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { fetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
|
||||
import { renderWithAccountProfile as render } from '@/test/console/account-profile'
|
||||
import { AppACLPermission } from '@/utils/permission'
|
||||
import Panel from '../panel'
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
appPermissionKeys: [] as string[],
|
||||
workspacePermissionKeys: [] as string[],
|
||||
configButtonProps: [] as Array<{
|
||||
readOnly: boolean
|
||||
hasConfigured: boolean
|
||||
}>,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/workspace-state', async () => {
|
||||
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
|
||||
return createWorkspaceStateModuleMock(() => ({
|
||||
currentWorkspace: { id: 'workspace-1' },
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
usePathname: () => '/app/app-1/overview',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: vi.fn((selector: (state: { appDetail: { permission_keys: string[] } }) => unknown) =>
|
||||
selector({
|
||||
appDetail: {
|
||||
permission_keys: testState.appPermissionKeys,
|
||||
},
|
||||
}),
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/apps', () => ({
|
||||
fetchTracingStatus: vi.fn(),
|
||||
fetchTracingConfig: vi.fn(),
|
||||
@@ -79,24 +54,13 @@ const mockedFetchTracingStatus = vi.mocked(fetchTracingStatus)
|
||||
const mockedFetchTracingConfig = vi.mocked(fetchTracingConfig)
|
||||
const mockedUpdateTracingStatus = vi.mocked(updateTracingStatus)
|
||||
|
||||
const renderPanel = async () => {
|
||||
render(<Panel />)
|
||||
|
||||
const renderPanel = async (readOnly = true) => {
|
||||
render(<Panel appId="app-1" readOnly={readOnly} />)
|
||||
await screen.findAllByTestId('config-button')
|
||||
}
|
||||
|
||||
vi.mock('@/context/permission-state', async () => {
|
||||
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
|
||||
|
||||
return createPermissionStateModuleMock(() => ({
|
||||
workspacePermissionKeys: [],
|
||||
}))
|
||||
})
|
||||
|
||||
describe('Tracing overview panel permissions', () => {
|
||||
describe('Tracing overview panel', () => {
|
||||
beforeEach(() => {
|
||||
testState.appPermissionKeys = []
|
||||
testState.workspacePermissionKeys = []
|
||||
testState.configButtonProps = []
|
||||
mockedFetchTracingStatus.mockResolvedValue({
|
||||
enabled: false,
|
||||
@@ -116,7 +80,7 @@ describe('Tracing overview panel permissions', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('marks tracing config as read-only without app monitor or workspace tracking permissions', async () => {
|
||||
it('marks tracing config as read-only when requested by its owner', async () => {
|
||||
await renderPanel()
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -127,23 +91,8 @@ describe('Tracing overview panel permissions', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('marks tracing config as read-only with app monitor permission only', async () => {
|
||||
testState.appPermissionKeys = [AppACLPermission.Monitor]
|
||||
|
||||
await renderPanel()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(testState.configButtonProps[0]).toMatchObject({
|
||||
readOnly: true,
|
||||
hasConfigured: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('allows tracing config when app ACL includes tracing config permission', async () => {
|
||||
testState.appPermissionKeys = [AppACLPermission.TracingConfig]
|
||||
|
||||
await renderPanel()
|
||||
it('allows tracing config when the owner grants write access', async () => {
|
||||
await renderPanel(false)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(testState.configButtonProps[0]).toMatchObject({
|
||||
|
||||
@@ -17,53 +17,30 @@ import type { TracingStatus } from '@/models/app'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Separator } from '@langgenius/dify-ui/separator'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
import {
|
||||
fetchTracingConfig as doFetchTracingConfig,
|
||||
fetchTracingStatus,
|
||||
updateTracingStatus,
|
||||
} from '@/service/apps'
|
||||
import { getAppACLCapabilities } from '@/utils/permission'
|
||||
import ConfigButton from './config-button'
|
||||
import TracingIcon from './tracing-icon'
|
||||
import { TracingProvider } from './type'
|
||||
|
||||
const I18N_PREFIX = 'tracing'
|
||||
|
||||
const Panel: FC = () => {
|
||||
const { t } = useTranslation(['app', 'common'])
|
||||
const pathname = usePathname()
|
||||
const matched = /\/app\/([^/]+)/.exec(pathname)
|
||||
const appId = matched?.length && matched[1] ? matched[1] : ''
|
||||
const { data: currentUserId } = useSuspenseQuery({
|
||||
...userProfileQueryOptions(),
|
||||
select: (data) => data.profile.id,
|
||||
})
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const appDetail = useAppStore((s) => s.appDetail)
|
||||
const appACLCapabilities = React.useMemo(
|
||||
() =>
|
||||
getAppACLCapabilities(appDetail?.permission_keys, {
|
||||
currentUserId,
|
||||
resourceMaintainer: appDetail?.maintainer,
|
||||
workspacePermissionKeys,
|
||||
}),
|
||||
[appDetail?.maintainer, appDetail?.permission_keys, currentUserId, workspacePermissionKeys],
|
||||
)
|
||||
const canConfigTracing = appACLCapabilities.canConfigureTracing
|
||||
const readOnly = !canConfigTracing
|
||||
type PanelProps = {
|
||||
appId: string
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
const Panel: FC<PanelProps> = ({ appId, readOnly }) => {
|
||||
const { t } = useTranslation(['app', 'common'])
|
||||
const [isLoaded, { setTrue: setLoaded }] = useBoolean(false)
|
||||
|
||||
const [tracingStatus, setTracingStatus] = useState<TracingStatus | null>(null)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useSuspenseQueries } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import * as React from 'react'
|
||||
import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { getAppACLCapabilities } from '@/utils/permission'
|
||||
import ChartView from './chart-view'
|
||||
import TracingPanel from './tracing/panel'
|
||||
@@ -16,23 +16,27 @@ type OverviewViewProps = {
|
||||
}
|
||||
|
||||
const OverviewView = ({ appId }: OverviewViewProps) => {
|
||||
const appDetail = useAppStore((state) => state.appDetail)
|
||||
const { data: currentUserId } = useSuspenseQuery({
|
||||
...userProfileQueryOptions(),
|
||||
select: (data) => data.profile.id,
|
||||
const [{ data: appDetail }, { data: userProfile }] = useSuspenseQueries({
|
||||
queries: [
|
||||
consoleQuery.apps.byAppId.get.queryOptions({
|
||||
input: { params: { app_id: appId } },
|
||||
}),
|
||||
userProfileQueryOptions(),
|
||||
],
|
||||
})
|
||||
const currentUserId = userProfile.profile.id
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const appACLCapabilities = React.useMemo(
|
||||
() =>
|
||||
getAppACLCapabilities(appDetail?.permission_keys, {
|
||||
getAppACLCapabilities(appDetail.permission_keys, {
|
||||
currentUserId,
|
||||
resourceMaintainer: appDetail?.maintainer,
|
||||
resourceMaintainer: appDetail.maintainer,
|
||||
workspacePermissionKeys,
|
||||
}),
|
||||
[appDetail?.maintainer, appDetail?.permission_keys, currentUserId, workspacePermissionKeys],
|
||||
[appDetail.maintainer, appDetail.permission_keys, currentUserId, workspacePermissionKeys],
|
||||
)
|
||||
|
||||
if (!appDetail || !appACLCapabilities.canMonitor) return null
|
||||
if (!appACLCapabilities.canMonitor) return null
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
@@ -40,7 +44,12 @@ const OverviewView = ({ appId }: OverviewViewProps) => {
|
||||
<div className="min-h-0 flex-1">
|
||||
<ChartView
|
||||
appId={appId}
|
||||
headerRight={appACLCapabilities.canConfigureTracing ? <TracingPanel /> : null}
|
||||
appMode={appDetail.mode}
|
||||
headerRight={
|
||||
appACLCapabilities.canConfigureTracing ? (
|
||||
<TracingPanel appId={appId} readOnly={false} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user