fix(dataset): align New RAG activity filters

This commit is contained in:
Stephen Zhou 2026-07-31 17:00:59 +08:00
parent e891790ce0
commit abee50b90e
No known key found for this signature in database
16 changed files with 379 additions and 35 deletions

View File

@ -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,

View File

@ -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)),

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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",

View File

@ -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)}`);

View File

@ -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",
}),

View File

@ -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,
}),

View File

@ -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 } : {}),

View File

@ -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",
});

View File

@ -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)

View File

@ -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

View File

@ -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),

View File

@ -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', () => {

View File

@ -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>
)