mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
fix(dataset): align New RAG activity filters
This commit is contained in:
parent
e891790ce0
commit
abee50b90e
@ -1040,6 +1040,8 @@ class KnowledgeFSSpaceOverviewActivityApi(Resource):
|
||||
account_id=actor_id,
|
||||
control_space_id=control_space_id,
|
||||
action=query.action,
|
||||
actor_id=query.actor_id,
|
||||
actor_type=query.actor_type,
|
||||
cursor=query.cursor,
|
||||
from_at=query.from_at.astimezone(UTC).isoformat().replace("+00:00", "Z") if query.from_at else None,
|
||||
limit=query.limit,
|
||||
|
||||
@ -257,6 +257,8 @@ class KnowledgeFSDataFacade:
|
||||
account_id: str,
|
||||
control_space_id: str,
|
||||
action: str | None,
|
||||
actor_id: str | None,
|
||||
actor_type: str | None,
|
||||
cursor: str | None,
|
||||
from_at: str | None,
|
||||
limit: int,
|
||||
@ -268,6 +270,8 @@ class KnowledgeFSDataFacade:
|
||||
(key, value)
|
||||
for key, value in (
|
||||
("action", action),
|
||||
("actorId", actor_id),
|
||||
("actorType", actor_type),
|
||||
("cursor", cursor),
|
||||
("from", from_at),
|
||||
("limit", str(limit)),
|
||||
|
||||
@ -418,6 +418,8 @@ class KnowledgeFSOverviewActivityListQuery(BaseModel):
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
actor_id: str | None = Field(default=None, min_length=1, max_length=255)
|
||||
actor_type: Literal["member", "system"] | None = None
|
||||
cursor: str | None = Field(default=None, min_length=1, max_length=512)
|
||||
from_at: datetime | None = Field(default=None, validation_alias=AliasChoices("from_at", "from"))
|
||||
limit: int = Field(default=50, ge=1, le=100)
|
||||
@ -441,6 +443,8 @@ class KnowledgeFSOverviewActivityListQuery(BaseModel):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_time_range(self) -> KnowledgeFSOverviewActivityListQuery:
|
||||
if self.actor_type == "system" and self.actor_id is not None:
|
||||
raise ValueError("actor_id cannot be combined with the system actor type")
|
||||
if self.from_at is not None and self.to_at is not None and self.from_at > self.to_at:
|
||||
raise ValueError("from must not be after to")
|
||||
return self
|
||||
|
||||
@ -198,6 +198,8 @@ _CONSOLE_DELEGATION_CASES = (
|
||||
{
|
||||
"control_space_id": "space-1",
|
||||
"action": None,
|
||||
"actor_id": None,
|
||||
"actor_type": None,
|
||||
"cursor": None,
|
||||
"from_at": None,
|
||||
"limit": 50,
|
||||
|
||||
@ -1117,6 +1117,8 @@ def test_advanced_facade_binds_child_resources_parent_space_and_idempotency() ->
|
||||
"listOverviewActivity",
|
||||
{
|
||||
"action": None,
|
||||
"actor_id": None,
|
||||
"actor_type": None,
|
||||
"cursor": "cursor-1",
|
||||
"from_at": "2026-07-01T00:00:00Z",
|
||||
"limit": 20,
|
||||
|
||||
@ -639,6 +639,8 @@ describe.each(["postgres", "tidb"] as const)(
|
||||
|
||||
const page = await repository.listActivity({
|
||||
action: "query.requested",
|
||||
actorId: "editor-1",
|
||||
actorType: "member",
|
||||
candidateGrants: ["team:camera"],
|
||||
cursor: { id: QUERY_ID, occurredAt: NOW },
|
||||
from: "2026-07-13T14:00:00.000Z",
|
||||
@ -657,6 +659,8 @@ describe.each(["postgres", "tidb"] as const)(
|
||||
SPACE_ID,
|
||||
JSON.stringify(["team:camera"]),
|
||||
"query.requested",
|
||||
"member",
|
||||
"editor-1",
|
||||
"query",
|
||||
"pending",
|
||||
"2026-07-13T14:00:00.000Z",
|
||||
|
||||
@ -93,6 +93,14 @@ export function createDatabaseKnowledgeSpaceOverviewRepository({
|
||||
params.push(input.action);
|
||||
filters.push(`event.${q(database, "action")} = ${p(database, params.length)}`);
|
||||
}
|
||||
if (input.actorType) {
|
||||
params.push(input.actorType);
|
||||
filters.push(`event.${q(database, "actor_type")} = ${p(database, params.length)}`);
|
||||
}
|
||||
if (input.actorId) {
|
||||
params.push(input.actorId);
|
||||
filters.push(`event.${q(database, "actor_subject_id")} = ${p(database, params.length)}`);
|
||||
}
|
||||
if (input.resourceType) {
|
||||
params.push(input.resourceType);
|
||||
filters.push(`event.${q(database, "resource_type")} = ${p(database, params.length)}`);
|
||||
|
||||
@ -97,6 +97,8 @@ describe("knowledge-space Overview handler branch coverage", () => {
|
||||
activityResult: { items: [activity()], nextCursor },
|
||||
query: {
|
||||
action: "document.failed",
|
||||
actorId: "dify-account:editor-1",
|
||||
actorType: "member",
|
||||
cursor: encodeKnowledgeSpaceActivityCursor(nextCursor),
|
||||
from: "2026-07-13T12:00:00.000Z",
|
||||
limit: 5,
|
||||
@ -114,6 +116,8 @@ describe("knowledge-space Overview handler branch coverage", () => {
|
||||
expect(fixture.overview.listActivity).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "document.failed",
|
||||
actorId: "dify-account:editor-1",
|
||||
actorType: "member",
|
||||
resourceType: "document",
|
||||
result: "failure",
|
||||
}),
|
||||
|
||||
@ -25,7 +25,7 @@ describe("knowledge-space Overview HTTP API", () => {
|
||||
const fixture = await createFixture();
|
||||
|
||||
const response = await fixture.app.request(
|
||||
`/knowledge-spaces/${SPACE_ID}/overview/activity?limit=5`,
|
||||
`/knowledge-spaces/${SPACE_ID}/overview/activity?limit=5&actorType=member&actorId=dify-account%3Aviewer-1`,
|
||||
{ headers: { authorization: "Bearer viewer-token" } },
|
||||
);
|
||||
|
||||
@ -40,6 +40,8 @@ describe("knowledge-space Overview HTTP API", () => {
|
||||
expect(fixture.listActivity).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
candidateGrants: expect.any(Array),
|
||||
actorId: "dify-account:viewer-1",
|
||||
actorType: "member",
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
tenantId: TENANT_ID,
|
||||
}),
|
||||
|
||||
@ -107,6 +107,8 @@ export function registerKnowledgeSpaceOverviewHandlers(input: {
|
||||
try {
|
||||
const result = await input.overview.listActivity({
|
||||
...(query.action ? { action: query.action } : {}),
|
||||
...(query.actorId ? { actorId: query.actorId } : {}),
|
||||
...(query.actorType ? { actorType: query.actorType } : {}),
|
||||
candidateGrants: scope.candidateGrants,
|
||||
...(query.cursor ? { cursor: decodeKnowledgeSpaceActivityCursor(query.cursor) } : {}),
|
||||
...(query.from ? { from: query.from } : {}),
|
||||
|
||||
@ -27,6 +27,8 @@ export const KnowledgeSpaceAttentionParamsSchema = z.object({
|
||||
export const ListKnowledgeSpaceActivityQuerySchema = z
|
||||
.object({
|
||||
action: z.enum(KnowledgeSpaceActivityActions).optional(),
|
||||
actorId: z.string().min(1).max(255).optional(),
|
||||
actorType: z.enum(["member", "system"]).optional(),
|
||||
cursor: z.string().min(1).max(512).optional(),
|
||||
from: IsoDateTimeSchema.optional(),
|
||||
limit: BoundedOverviewLimitSchema,
|
||||
@ -35,6 +37,9 @@ export const ListKnowledgeSpaceActivityQuerySchema = z
|
||||
to: IsoDateTimeSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((value) => value.actorType !== "system" || !value.actorId, {
|
||||
message: "actorId cannot be combined with the system actor type",
|
||||
})
|
||||
.refine((value) => !value.from || !value.to || value.from <= value.to, {
|
||||
message: "from must not be after to",
|
||||
});
|
||||
|
||||
@ -68,6 +68,8 @@ export interface KnowledgeSpaceActivityCursor {
|
||||
|
||||
export interface ListKnowledgeSpaceActivityInput {
|
||||
readonly action?: KnowledgeSpaceActivityAction | undefined;
|
||||
readonly actorId?: string | undefined;
|
||||
readonly actorType?: "member" | "system" | undefined;
|
||||
readonly candidateGrants: readonly string[];
|
||||
readonly cursor?: KnowledgeSpaceActivityCursor | undefined;
|
||||
readonly from?: string | undefined;
|
||||
@ -398,6 +400,8 @@ export function createInMemoryKnowledgeSpaceOverviewRepository(options: {
|
||||
candidatePermissionScopeAllows(event.requiredPermissionScope, input.candidateGrants),
|
||||
)
|
||||
.filter((event) => !input.action || event.action === input.action)
|
||||
.filter((event) => !input.actorType || event.actor.type === input.actorType)
|
||||
.filter((event) => !input.actorId || event.actor.id === input.actorId)
|
||||
.filter((event) => !input.resourceType || event.resource.type === input.resourceType)
|
||||
.filter((event) => !input.result || event.result === input.result)
|
||||
.filter((event) => !input.from || event.occurredAt >= input.from)
|
||||
|
||||
@ -2237,6 +2237,8 @@ export type GetKnowledgeFsSpacesByControlSpaceIdOverviewActivityData = {
|
||||
| 'source.failed'
|
||||
| 'source.synced'
|
||||
| 'worker.failed'
|
||||
actor_id?: string
|
||||
actor_type?: 'member' | 'system'
|
||||
cursor?: string
|
||||
from_at?: string
|
||||
limit?: number
|
||||
|
||||
@ -2536,6 +2536,8 @@ export const zGetKnowledgeFsSpacesByControlSpaceIdOverviewActivityQuery = z.obje
|
||||
'worker.failed',
|
||||
])
|
||||
.optional(),
|
||||
actor_id: z.string().min(1).max(255).optional(),
|
||||
actor_type: z.enum(['member', 'system']).optional(),
|
||||
cursor: z.string().min(1).max(512).optional(),
|
||||
from_at: z.iso.datetime().optional(),
|
||||
limit: z.int().gte(1).lte(100).optional().default(50),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import dayjs from 'dayjs'
|
||||
import { renderWithNuqs } from '@/test/nuqs-testing'
|
||||
import { KnowledgeOverviewPage } from '../overview/knowledge-overview-page'
|
||||
|
||||
@ -11,6 +12,11 @@ const queryOptionsMocks = vi.hoisted(() => ({
|
||||
stats: vi.fn(),
|
||||
}))
|
||||
|
||||
const infiniteOptionsMocks = vi.hoisted(() => ({
|
||||
activity: vi.fn(),
|
||||
tasks: vi.fn(),
|
||||
}))
|
||||
|
||||
const chartOptions = vi.hoisted(() => ({ current: undefined as unknown }))
|
||||
|
||||
const queryData = vi.hoisted(() => ({
|
||||
@ -19,7 +25,7 @@ const queryData = vi.hoisted(() => ({
|
||||
{
|
||||
action: 'source.synced',
|
||||
actor: { id: 'dify-account:member-1', type: 'member' },
|
||||
details: { count: 1 },
|
||||
details: { count: 1 } as Record<string, boolean | number | string>,
|
||||
id: 'activity-1',
|
||||
occurred_at: '2026-07-29T08:05:00Z',
|
||||
resource: { id: 'source-1', type: 'source' },
|
||||
@ -258,14 +264,20 @@ vi.mock('@/service/client', () => {
|
||||
},
|
||||
},
|
||||
get: {
|
||||
infiniteOptions: () => ({ queryKey: ['tasksInfinite'] }),
|
||||
infiniteOptions: (options: unknown) => {
|
||||
infiniteOptionsMocks.tasks(options)
|
||||
return { ...(options as object), queryKey: ['tasksInfinite'] }
|
||||
},
|
||||
},
|
||||
},
|
||||
overview: {
|
||||
activity: {
|
||||
get: {
|
||||
...query('activity'),
|
||||
infiniteOptions: () => ({ queryKey: ['activityInfinite'] }),
|
||||
infiniteOptions: (options: unknown) => {
|
||||
infiniteOptionsMocks.activity(options)
|
||||
return { ...(options as object), queryKey: ['activityInfinite'] }
|
||||
},
|
||||
},
|
||||
},
|
||||
attention: { get: query('attention') },
|
||||
@ -516,6 +528,9 @@ describe('KnowledgeOverviewPage', () => {
|
||||
it('opens the complete activity view from recent activity', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWithNuqs(<KnowledgeOverviewPage knowledgeSpaceId="space-1" />)
|
||||
const whenHeader = screen.getByRole('columnheader', {
|
||||
name: 'dataset.newKnowledge.overview.when',
|
||||
})
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'dataset.newKnowledge.overview.allActivity' }),
|
||||
@ -530,8 +545,92 @@ describe('KnowledgeOverviewPage', () => {
|
||||
screen.getByRole('combobox', { name: 'dataset.newKnowledge.overview.timeRange' }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('combobox', { name: 'dataset.newKnowledge.overview.operator' }),
|
||||
).not.toBeInTheDocument()
|
||||
screen.getByRole('combobox', { name: 'dataset.newKnowledge.overview.operator' }),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('group', { name: 'dataset.newKnowledge.overview.timeRange' }),
|
||||
).toBeInTheDocument()
|
||||
expect(document.querySelector('.bg-transparent')).toBeInTheDocument()
|
||||
expect(whenHeader).not.toHaveClass('opacity-0')
|
||||
})
|
||||
|
||||
it('restarts activity pagination when the date or operator filter changes', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWithNuqs(<KnowledgeOverviewPage knowledgeSpaceId="space-1" />)
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'dataset.newKnowledge.overview.allActivity' }),
|
||||
)
|
||||
|
||||
const operator = screen.getByRole('combobox', {
|
||||
name: 'dataset.newKnowledge.overview.operator',
|
||||
})
|
||||
await user.click(operator)
|
||||
await user.click(await screen.findByRole('option', { name: 'Ada' }))
|
||||
|
||||
const memberOptions = infiniteOptionsMocks.activity.mock.lastCall?.[0] as {
|
||||
input: (cursor: string | null) => {
|
||||
query: Record<string, unknown>
|
||||
}
|
||||
queryKey: unknown[]
|
||||
}
|
||||
expect(memberOptions.queryKey).toEqual([
|
||||
'knowledge-fs-overview-activity',
|
||||
'space-1',
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
'member:member-1',
|
||||
])
|
||||
expect(memberOptions.input(null).query).toMatchObject({
|
||||
actor_id: 'dify-account:member-1',
|
||||
actor_type: 'member',
|
||||
limit: 20,
|
||||
})
|
||||
expect(memberOptions.input('older').query.cursor).toBe('older')
|
||||
|
||||
const timeRange = screen.getByRole('combobox', {
|
||||
name: 'dataset.newKnowledge.overview.timeRange',
|
||||
})
|
||||
await user.click(timeRange)
|
||||
await user.click(
|
||||
await screen.findByRole('option', { name: 'dataset.newKnowledge.overview.today' }),
|
||||
)
|
||||
|
||||
const todayOptions = infiniteOptionsMocks.activity.mock.lastCall?.[0] as {
|
||||
input: (cursor: string | null) => {
|
||||
query: Record<string, unknown>
|
||||
}
|
||||
queryKey: unknown[]
|
||||
}
|
||||
expect(todayOptions.queryKey[4]).toBe('member:member-1')
|
||||
expect(todayOptions.input(null).query).toMatchObject({
|
||||
actor_id: 'dify-account:member-1',
|
||||
actor_type: 'member',
|
||||
from_at: expect.any(String),
|
||||
to_at: expect.any(String),
|
||||
})
|
||||
expect(dayjs(todayOptions.input(null).query.from_at as string).isSame(dayjs(), 'day')).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it('shows safe activity details and relative times for today in the drawer', async () => {
|
||||
const user = userEvent.setup()
|
||||
queryData.activity.data[0]!.action = 'source.failed'
|
||||
queryData.activity.data[0]!.details = { reasonCode: 'CREDENTIALS_EXPIRED' }
|
||||
queryData.activity.data[0]!.occurred_at = new Date(Date.now() - 2 * 60 * 60_000).toISOString()
|
||||
queryData.activity.data[0]!.result = 'failure'
|
||||
|
||||
renderWithNuqs(<KnowledgeOverviewPage knowledgeSpaceId="space-1" />)
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'dataset.newKnowledge.overview.allActivity' }),
|
||||
)
|
||||
|
||||
const dialog = screen.getByRole('dialog', {
|
||||
name: 'dataset.newKnowledge.overview.allActivity',
|
||||
})
|
||||
expect(dialog).toHaveTextContent('Credentials expired')
|
||||
expect(dialog).toHaveTextContent('dataset.newKnowledge.overview.activityFailed')
|
||||
expect(within(dialog).getByText(/2h ago|2 hr\. ago|2 hours ago/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('refreshes overview snapshots until they catch up with a completed background task', () => {
|
||||
|
||||
@ -6,8 +6,10 @@ import type {
|
||||
KnowledgeFsOverviewAttentionResponse,
|
||||
KnowledgeFsOverviewQueryOutcomeBucketResponse,
|
||||
} from '@dify/contracts/api/console/knowledge-fs/types.gen'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import type { CSSProperties } from 'react'
|
||||
import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types'
|
||||
import type { Member } from '@/models/common'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
@ -38,6 +40,7 @@ import { useAtomValue } from 'jotai'
|
||||
import { parseAsStringLiteral, useQueryState } from 'nuqs'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import {
|
||||
datasetDefaultPermissionKeysAtom,
|
||||
@ -57,9 +60,12 @@ import {
|
||||
} from '../routes'
|
||||
|
||||
type OverviewWindow = '24h' | '7d' | '30d'
|
||||
type ActivityRange = 'today' | '7d' | '30d' | '90d' | 'all'
|
||||
type ActivityRange = 'today' | '7d' | '30d' | '90d' | 'all' | 'custom'
|
||||
type ActivityOperator = 'all' | 'system' | `member:${string}`
|
||||
type ActivityDateRange = { end: Dayjs; start: Dayjs }
|
||||
|
||||
const WINDOWS: OverviewWindow[] = ['24h', '7d', '30d']
|
||||
const ACTIVITY_RANGES: ActivityRange[] = ['today', '7d', '30d', '90d', 'all', 'custom']
|
||||
const QUERY_OUTCOMES_WINDOW: OverviewWindow = '7d'
|
||||
const ACTIVE_TASK_STATES = new Set<KnowledgeFsBackgroundTaskResponse['state']>([
|
||||
'queued',
|
||||
@ -72,6 +78,18 @@ const overviewWindowParser = parseAsStringLiteral(WINDOWS)
|
||||
.withDefault('24h')
|
||||
.withOptions({ history: 'push' })
|
||||
|
||||
function activityDatesForRange(range: Exclude<ActivityRange, 'custom'>): ActivityDateRange {
|
||||
const end = dayjs().endOf('day')
|
||||
if (range === 'all') return { end, start: dayjs(0) }
|
||||
if (range === 'today') return { end, start: dayjs().startOf('day') }
|
||||
return {
|
||||
end,
|
||||
start: dayjs()
|
||||
.subtract(Number.parseInt(range) - 1, 'day')
|
||||
.startOf('day'),
|
||||
}
|
||||
}
|
||||
|
||||
function isFirstSourceTask(task: KnowledgeFsBackgroundTaskResponse) {
|
||||
return (
|
||||
task.operation === 'document_processing' ||
|
||||
@ -629,15 +647,35 @@ function activityLabel(
|
||||
t: ReturnType<typeof useTranslation<'dataset'>>['t'],
|
||||
) {
|
||||
const operation = activityOperationLabel(activity, t)
|
||||
let label: string
|
||||
if (activity.result === 'success')
|
||||
return t(($) => $['newKnowledge.overview.activityCompleted'], { operation })
|
||||
if (activity.result === 'failure')
|
||||
return t(($) => $['newKnowledge.overview.activityFailed'], { operation })
|
||||
if (activity.result === 'canceled')
|
||||
return t(($) => $['newKnowledge.overview.activityCanceled'], { operation })
|
||||
return activity.action === 'query.requested'
|
||||
? t(($) => $['newKnowledge.overview.activityQueued'], { operation })
|
||||
: t(($) => $['newKnowledge.overview.activityRunning'], { operation })
|
||||
label = t(($) => $['newKnowledge.overview.activityCompleted'], { operation })
|
||||
else if (activity.result === 'failure')
|
||||
label = t(($) => $['newKnowledge.overview.activityFailed'], { operation })
|
||||
else if (activity.result === 'canceled')
|
||||
label = t(($) => $['newKnowledge.overview.activityCanceled'], { operation })
|
||||
else
|
||||
label =
|
||||
activity.action === 'query.requested'
|
||||
? t(($) => $['newKnowledge.overview.activityQueued'], { operation })
|
||||
: t(($) => $['newKnowledge.overview.activityRunning'], { operation })
|
||||
|
||||
const detail = [
|
||||
activity.details.reasonCode,
|
||||
activity.details.statusCode,
|
||||
activity.details.documentType,
|
||||
activity.details.providerId,
|
||||
activity.details.mode,
|
||||
].find((value): value is string => typeof value === 'string' && Boolean(value.trim()))
|
||||
if (!detail) return label
|
||||
|
||||
const readableDetail = /^[A-Z0-9_]+$/.test(detail)
|
||||
? detail
|
||||
.toLocaleLowerCase()
|
||||
.replaceAll('_', ' ')
|
||||
.replace(/^./, (character) => character.toLocaleUpperCase())
|
||||
: detail
|
||||
return `${label} — ${readableDetail}`
|
||||
}
|
||||
|
||||
function compactIdentifier(value: string) {
|
||||
@ -809,9 +847,7 @@ function RecentActivity({
|
||||
role="row"
|
||||
className="grid grid-cols-[100px_minmax(280px,1fr)_200px] items-center gap-3 pb-2 system-2xs-medium-uppercase text-text-tertiary"
|
||||
>
|
||||
<span role="columnheader" className="opacity-0">
|
||||
{t(($) => $['newKnowledge.overview.when'])}
|
||||
</span>
|
||||
<span role="columnheader">{t(($) => $['newKnowledge.overview.when'])}</span>
|
||||
<span role="columnheader">{t(($) => $['newKnowledge.overview.activity'])}</span>
|
||||
<span role="columnheader">{t(($) => $['newKnowledge.overview.operator'])}</span>
|
||||
</div>
|
||||
@ -838,10 +874,6 @@ function RecentActivity({
|
||||
{formatWhen(activity.occurred_at)}
|
||||
</span>
|
||||
<span role="cell" className="min-w-0 truncate text-text-secondary">
|
||||
<strong className="font-semibold text-text-primary">
|
||||
{activityOperationLabel(activity, t)}
|
||||
</strong>
|
||||
{' — '}
|
||||
{activityLabel(activity, t)}
|
||||
</span>
|
||||
<span role="cell" className="flex min-w-0 items-center gap-2">
|
||||
@ -862,31 +894,109 @@ function RecentActivity({
|
||||
)
|
||||
}
|
||||
|
||||
function ActivityDateRangePicker({
|
||||
dates,
|
||||
onChange,
|
||||
}: {
|
||||
dates: ActivityDateRange
|
||||
onChange: (dates: ActivityDateRange) => void
|
||||
}) {
|
||||
const { t, i18n } = useTranslation('dataset')
|
||||
const today = dayjs()
|
||||
const formatter = useMemo(
|
||||
() => new Intl.DateTimeFormat(i18n.language, { day: 'numeric', month: 'short' }),
|
||||
[i18n.language],
|
||||
)
|
||||
const renderTrigger =
|
||||
(edge: 'start' | 'end') =>
|
||||
({ handleClickTrigger, isOpen, value }: TriggerProps) => (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${t(($) => $['newKnowledge.overview.timeRange'])} ${edge}`}
|
||||
aria-expanded={isOpen}
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate rounded px-1 py-0.5 text-left system-xs-regular text-components-input-text-filled outline-hidden hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active',
|
||||
isOpen && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={handleClickTrigger}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return
|
||||
event.preventDefault()
|
||||
handleClickTrigger(event as unknown as React.MouseEvent)
|
||||
}}
|
||||
>
|
||||
{value ? formatter.format(value.toDate()) : '—'}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label={t(($) => $['newKnowledge.overview.timeRange'])}
|
||||
className="flex h-6 w-35 shrink-0 items-center rounded-lg bg-background-section px-1"
|
||||
>
|
||||
<DatePicker
|
||||
noConfirm
|
||||
needTimePicker={false}
|
||||
value={dates.start}
|
||||
onChange={(start) => start && onChange({ end: dates.end, start: start.startOf('day') })}
|
||||
onClear={() => undefined}
|
||||
renderTrigger={renderTrigger('start')}
|
||||
getIsDateDisabled={(date) => date.isAfter(today, 'day') || date.isAfter(dates.end, 'day')}
|
||||
/>
|
||||
<span aria-hidden className="text-text-quaternary">
|
||||
–
|
||||
</span>
|
||||
<DatePicker
|
||||
noConfirm
|
||||
needTimePicker={false}
|
||||
value={dates.end}
|
||||
onChange={(end) => end && onChange({ end: end.endOf('day'), start: dates.start })}
|
||||
onClear={() => undefined}
|
||||
renderTrigger={renderTrigger('end')}
|
||||
getIsDateDisabled={(date) =>
|
||||
date.isAfter(today, 'day') || date.isBefore(dates.start, 'day')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ActivityDrawer({
|
||||
activities,
|
||||
dates,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
loading,
|
||||
members,
|
||||
onDatesChange,
|
||||
onFetchNextPage,
|
||||
onOpenChange,
|
||||
onOperatorChange,
|
||||
onRangeChange,
|
||||
open,
|
||||
operator,
|
||||
range,
|
||||
}: {
|
||||
activities: KnowledgeFsOverviewActivityResponse[]
|
||||
dates: ActivityDateRange
|
||||
hasNextPage: boolean
|
||||
isFetchingNextPage: boolean
|
||||
loading: boolean
|
||||
members: Member[]
|
||||
onDatesChange: (dates: ActivityDateRange) => void
|
||||
onFetchNextPage: () => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
onOperatorChange: (operator: ActivityOperator) => void
|
||||
onRangeChange: (range: ActivityRange) => void
|
||||
open: boolean
|
||||
operator: ActivityOperator
|
||||
range: ActivityRange
|
||||
}) {
|
||||
const { t, i18n } = useTranslation('dataset')
|
||||
const { t: tDeployments } = useTranslation('deployments')
|
||||
const { t: tActivityLog } = useTranslation('appLog')
|
||||
const rangeTriggerRef = useRef<HTMLButtonElement>(null)
|
||||
const restoreFilterFocusRef = useRef(false)
|
||||
const now = dayjs()
|
||||
@ -903,6 +1013,10 @@ function ActivityDrawer({
|
||||
() => new Intl.DateTimeFormat(i18n.language, { hour: 'numeric', minute: '2-digit' }),
|
||||
[i18n.language],
|
||||
)
|
||||
const relativeTimeFormatter = useMemo(
|
||||
() => new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto', style: 'narrow' }),
|
||||
[i18n.language],
|
||||
)
|
||||
const groups = activities.reduce<Record<string, KnowledgeFsOverviewActivityResponse[]>>(
|
||||
(result, task) => {
|
||||
const key = dayjs(task.occurred_at).format('YYYY-MM-DD')
|
||||
@ -919,16 +1033,37 @@ function ActivityDrawer({
|
||||
return t(($) => $['newKnowledge.overview.yesterday'])
|
||||
return dateFormatter.format(date.toDate())
|
||||
}
|
||||
const activityTime = (occurredAt: string) => {
|
||||
const occurred = dayjs(occurredAt)
|
||||
if (!occurred.isSame(now, 'day')) return timeFormatter.format(occurred.toDate())
|
||||
const elapsedMinutes = Math.max(0, now.diff(occurred, 'minute'))
|
||||
if (elapsedMinutes < 60) return relativeTimeFormatter.format(-elapsedMinutes, 'minute')
|
||||
return relativeTimeFormatter.format(-Math.floor(elapsedMinutes / 60), 'hour')
|
||||
}
|
||||
const rangeLabel: Record<ActivityRange, string> = {
|
||||
'30d': t(($) => $['newKnowledge.overview.last30Days']),
|
||||
'7d': t(($) => $['newKnowledge.overview.last7Days']),
|
||||
'90d': t(($) => $['newKnowledge.overview.last90Days']),
|
||||
all: t(($) => $['newKnowledge.overview.allTime']),
|
||||
custom: tActivityLog(($) => $['filter.period.custom']),
|
||||
today: t(($) => $['newKnowledge.overview.today']),
|
||||
}
|
||||
const rangeTriggerLabel: Record<ActivityRange, string> = {
|
||||
...rangeLabel,
|
||||
'30d': t(($) => $['newKnowledge.overview.thirtyDays']),
|
||||
'7d': t(($) => $['newKnowledge.overview.sevenDays']),
|
||||
'90d': '90d',
|
||||
}
|
||||
const operatorLabel =
|
||||
operator === 'all'
|
||||
? tActivityLog(($) => $['filter.annotation.all'])
|
||||
: operator === 'system'
|
||||
? t(($) => $['newKnowledge.overview.system'])
|
||||
: members.find((member) => `member:${member.id}` === operator)?.name || operator.slice(7)
|
||||
const clearFilters = () => {
|
||||
restoreFilterFocusRef.current = true
|
||||
onRangeChange('7d')
|
||||
onOperatorChange('all')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -940,7 +1075,7 @@ function ActivityDrawer({
|
||||
return (
|
||||
<Drawer open={open} swipeDirection="right" onOpenChange={onOpenChange}>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop />
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className="data-[swipe-direction=right]:w-120 data-[swipe-direction=right]:max-w-[calc(100vw-1rem)]">
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col bg-components-panel-bg p-0 pb-0">
|
||||
@ -952,7 +1087,7 @@ function ActivityDrawer({
|
||||
<DrawerCloseButton />
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex h-9 shrink-0 border-b border-divider-subtle px-5">
|
||||
<div className="flex h-9 shrink-0 items-start gap-1 border-b border-divider-subtle px-5">
|
||||
<Select
|
||||
value={range}
|
||||
onValueChange={(value) => onRangeChange(value as ActivityRange)}
|
||||
@ -960,12 +1095,12 @@ function ActivityDrawer({
|
||||
<SelectTrigger
|
||||
ref={rangeTriggerRef}
|
||||
aria-label={t(($) => $['newKnowledge.overview.timeRange'])}
|
||||
className="h-6 min-w-0 flex-1 border-0 bg-background-section shadow-none"
|
||||
className="h-6 w-20 min-w-0 shrink-0 border-0 bg-background-section shadow-none"
|
||||
>
|
||||
<span className="truncate">{rangeLabel[range]}</span>
|
||||
<span className="truncate">{rangeTriggerLabel[range]}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(rangeLabel) as ActivityRange[]).map((value) => (
|
||||
{ACTIVITY_RANGES.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
<SelectItemText>{rangeLabel[value]}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
@ -973,6 +1108,42 @@ function ActivityDrawer({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{range === 'all' ? (
|
||||
<div className="flex h-6 w-35 shrink-0 items-center rounded-lg bg-background-section px-2 system-xs-regular text-text-tertiary">
|
||||
{rangeLabel.all}
|
||||
</div>
|
||||
) : (
|
||||
<ActivityDateRangePicker dates={dates} onChange={onDatesChange} />
|
||||
)}
|
||||
<Select
|
||||
value={operator}
|
||||
onValueChange={(value) => onOperatorChange(value as ActivityOperator)}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t(($) => $['newKnowledge.overview.operator'])}
|
||||
className="h-6 w-50 min-w-0 shrink-0 border-0 bg-background-section shadow-none"
|
||||
>
|
||||
<span className="truncate">{operatorLabel}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
<SelectItemText>
|
||||
{tActivityLog(($) => $['filter.annotation.all'])}
|
||||
</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="system">
|
||||
<SelectItemText>{t(($) => $['newKnowledge.overview.system'])}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
{members.map((member) => (
|
||||
<SelectItem key={member.id} value={`member:${member.id}`}>
|
||||
<SelectItemText>{member.name || member.email}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
{loading ? (
|
||||
@ -1030,7 +1201,7 @@ function ActivityDrawer({
|
||||
className="shrink-0 system-xs-regular text-text-tertiary"
|
||||
dateTime={activity.occurred_at}
|
||||
>
|
||||
{timeFormatter.format(new Date(activity.occurred_at))}
|
||||
{activityTime(activity.occurred_at)}
|
||||
</time>
|
||||
</li>
|
||||
))}
|
||||
@ -1498,17 +1669,30 @@ export function KnowledgeOverviewPage({ knowledgeSpaceId }: { knowledgeSpaceId:
|
||||
const [window, setWindow] = useQueryState('window', overviewWindowParser)
|
||||
const [activityOpen, setActivityOpen] = useState(false)
|
||||
const [activityRange, setActivityRange] = useState<ActivityRange>('7d')
|
||||
const activityFrom = useMemo(() => {
|
||||
if (activityRange === 'all') return undefined
|
||||
if (activityRange === 'today') return dayjs().startOf('day').toISOString()
|
||||
return dayjs().subtract(Number.parseInt(activityRange), 'day').toISOString()
|
||||
}, [activityRange])
|
||||
const [activityDates, setActivityDates] = useState<ActivityDateRange>(() =>
|
||||
activityDatesForRange('7d'),
|
||||
)
|
||||
const [activityOperator, setActivityOperator] = useState<ActivityOperator>('all')
|
||||
const activityFrom = activityRange === 'all' ? undefined : activityDates.start.toISOString()
|
||||
const activityTo = activityRange === 'all' ? undefined : activityDates.end.toISOString()
|
||||
const activityActorType =
|
||||
activityOperator === 'all' ? undefined : activityOperator === 'system' ? 'system' : 'member'
|
||||
const activityActorId = activityOperator.startsWith('member:')
|
||||
? `dify-account:${activityOperator.slice(7)}`
|
||||
: undefined
|
||||
const handleActivityRangeChange = (range: ActivityRange) => {
|
||||
setActivityRange(range)
|
||||
if (range !== 'custom') setActivityDates(activityDatesForRange(range))
|
||||
}
|
||||
const handleActivityDatesChange = (dates: ActivityDateRange) => {
|
||||
setActivityDates(dates)
|
||||
setActivityRange('custom')
|
||||
}
|
||||
const membersQuery = useMembers()
|
||||
const tasksQuery = useInfiniteQuery(
|
||||
consoleQuery.knowledgeFs.spaces.byControlSpaceId.backgroundTasks.get.infiniteOptions({
|
||||
getNextPageParam: (lastPage) => lastPage.next_cursor,
|
||||
initialPageParam: null as string | null,
|
||||
queryKey: ['knowledge-fs-overview-activity', knowledgeSpaceId, activityRange],
|
||||
input: (pageParam) => ({
|
||||
params: { control_space_id: knowledgeSpaceId },
|
||||
query: {
|
||||
@ -1592,12 +1776,22 @@ export function KnowledgeOverviewPage({ knowledgeSpaceId }: { knowledgeSpaceId:
|
||||
consoleQuery.knowledgeFs.spaces.byControlSpaceId.overview.activity.get.infiniteOptions({
|
||||
getNextPageParam: (lastPage) => lastPage.next_cursor,
|
||||
initialPageParam: null as string | null,
|
||||
queryKey: [
|
||||
'knowledge-fs-overview-activity',
|
||||
knowledgeSpaceId,
|
||||
activityFrom,
|
||||
activityTo,
|
||||
activityOperator,
|
||||
],
|
||||
input: (pageParam) => ({
|
||||
params: { control_space_id: knowledgeSpaceId },
|
||||
query: {
|
||||
...(activityActorId ? { actor_id: activityActorId } : {}),
|
||||
...(activityActorType ? { actor_type: activityActorType } : {}),
|
||||
...(typeof pageParam === 'string' ? { cursor: pageParam } : {}),
|
||||
...(activityFrom ? { from_at: activityFrom } : {}),
|
||||
limit: ACTIVITY_PAGE_SIZE,
|
||||
...(activityTo ? { to_at: activityTo } : {}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
@ -1818,15 +2012,19 @@ export function KnowledgeOverviewPage({ knowledgeSpaceId }: { knowledgeSpaceId:
|
||||
</div>
|
||||
<ActivityDrawer
|
||||
activities={activities}
|
||||
dates={activityDates}
|
||||
hasNextPage={Boolean(activityDrawerQuery.hasNextPage)}
|
||||
isFetchingNextPage={activityDrawerQuery.isFetchingNextPage}
|
||||
loading={activityDrawerQuery.isPending || activityDrawerQuery.isRefetching}
|
||||
members={members}
|
||||
open={activityOpen}
|
||||
operator={activityOperator}
|
||||
range={activityRange}
|
||||
onDatesChange={handleActivityDatesChange}
|
||||
onFetchNextPage={() => void activityDrawerQuery.fetchNextPage()}
|
||||
onOpenChange={setActivityOpen}
|
||||
onRangeChange={setActivityRange}
|
||||
onOperatorChange={setActivityOperator}
|
||||
onRangeChange={handleActivityRangeChange}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user