mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-14 08:36:22 +08:00
feat(core): improve dashboard style (#15205)
This commit is contained in:
Binary file not shown.
@@ -1587,15 +1587,6 @@ test('should be able to manage context', async t => {
|
||||
buffer
|
||||
);
|
||||
|
||||
const { files } =
|
||||
(await listContextDocAndFiles(app, workspaceId, sessionId, contextId)) ||
|
||||
{};
|
||||
t.snapshot(
|
||||
cleanObject(files, ['id', 'error', 'createdAt']),
|
||||
'should list context files'
|
||||
);
|
||||
|
||||
// wait for processing
|
||||
await waitForStatus(
|
||||
async () =>
|
||||
(await listContextDocAndFiles(app, workspaceId, sessionId, contextId))
|
||||
@@ -1605,6 +1596,18 @@ test('should be able to manage context', async t => {
|
||||
60
|
||||
);
|
||||
|
||||
const { files } =
|
||||
(await listContextDocAndFiles(app, workspaceId, sessionId, contextId)) ||
|
||||
{};
|
||||
t.deepEqual(cleanObject(files, ['id', 'error', 'createdAt']), [
|
||||
{
|
||||
blobId: 'Ip3vuwzubwJnOlzeKQ0Gc-daDcMc7EOYnIqypOyn4bs',
|
||||
chunkSize: 1,
|
||||
name: 'sample.pdf',
|
||||
status: 'finished',
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await waitForMatches(
|
||||
() => matchFiles(app, contextId, 'test', 1),
|
||||
1
|
||||
@@ -1641,15 +1644,6 @@ test('should be able to manage context', async t => {
|
||||
|
||||
await addContextDoc(app, contextId, docId);
|
||||
|
||||
const { docs } =
|
||||
(await listContextDocAndFiles(app, workspaceId, sessionId, contextId)) ||
|
||||
{};
|
||||
t.snapshot(
|
||||
cleanObject(docs, ['error', 'createdAt']),
|
||||
'should list context docs'
|
||||
);
|
||||
|
||||
// wait for processing
|
||||
await waitForStatus(
|
||||
async () =>
|
||||
(await listContextDocAndFiles(app, workspaceId, sessionId, contextId))
|
||||
@@ -1659,6 +1653,16 @@ test('should be able to manage context', async t => {
|
||||
60
|
||||
);
|
||||
|
||||
const { docs } =
|
||||
(await listContextDocAndFiles(app, workspaceId, sessionId, contextId)) ||
|
||||
{};
|
||||
t.deepEqual(cleanObject(docs, ['error', 'createdAt']), [
|
||||
{
|
||||
id: docId,
|
||||
status: 'finished',
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await waitForMatches(
|
||||
() => matchWorkspaceDocs(app, contextId, 'test', 1),
|
||||
1
|
||||
|
||||
@@ -358,6 +358,12 @@ e2e(
|
||||
requestedSize
|
||||
effectiveSize
|
||||
}
|
||||
copilotWindow {
|
||||
to
|
||||
bucket
|
||||
requestedSize
|
||||
effectiveSize
|
||||
}
|
||||
syncActiveUsersTimeline {
|
||||
minute
|
||||
activeUsers
|
||||
@@ -366,6 +372,7 @@ e2e(
|
||||
date
|
||||
value
|
||||
}
|
||||
generatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -375,6 +382,7 @@ e2e(
|
||||
storageHistoryDays: -10,
|
||||
syncHistoryHours: -10,
|
||||
sharedLinkWindowDays: -10,
|
||||
copilotWindowDays: 500,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -385,6 +393,12 @@ e2e(
|
||||
t.is(dashboard.storageWindow.bucket, 'Day');
|
||||
t.is(dashboard.storageWindow.effectiveSize, 1);
|
||||
t.is(dashboard.topSharedLinksWindow.effectiveSize, 1);
|
||||
t.is(dashboard.copilotWindow.bucket, 'Day');
|
||||
t.is(dashboard.copilotWindow.effectiveSize, 90);
|
||||
t.is(
|
||||
new Date(dashboard.copilotWindow.to).getTime(),
|
||||
new Date(dashboard.generatedAt).getTime()
|
||||
);
|
||||
t.is(dashboard.syncActiveUsersTimeline.length, 1);
|
||||
t.is(dashboard.workspaceStorageHistory.length, 1);
|
||||
}
|
||||
|
||||
@@ -572,6 +572,43 @@ test('old canary date clientVersion should be rejected and disconnected in canar
|
||||
}
|
||||
});
|
||||
|
||||
test('canary date clientVersion should be rejected outside canary namespace', async t => {
|
||||
const prevNamespace = env.NAMESPACE;
|
||||
// @ts-expect-error test
|
||||
env.NAMESPACE = 'production';
|
||||
|
||||
try {
|
||||
const { user, cookieHeader } = await login(app);
|
||||
const spaceId = user.id;
|
||||
|
||||
const socket = createClient(url, cookieHeader);
|
||||
try {
|
||||
await waitForConnect(socket);
|
||||
|
||||
const res = unwrapResponse(
|
||||
t,
|
||||
await emitWithAck<{ clientId: string; success: boolean }>(
|
||||
socket,
|
||||
'space:join',
|
||||
{
|
||||
spaceType: 'userspace',
|
||||
spaceId,
|
||||
clientVersion: makeCanaryDateVersion(new Date(), '15'),
|
||||
}
|
||||
)
|
||||
);
|
||||
t.false(res.success);
|
||||
|
||||
await waitForDisconnect(socket);
|
||||
} finally {
|
||||
socket.disconnect();
|
||||
}
|
||||
} finally {
|
||||
// @ts-expect-error test
|
||||
env.NAMESPACE = prevNamespace;
|
||||
}
|
||||
});
|
||||
|
||||
test('space:join-awareness should reject clientVersion<0.25.0', async t => {
|
||||
const { user, cookieHeader } = await login(app);
|
||||
const spaceId = user.id;
|
||||
|
||||
@@ -12,6 +12,7 @@ type TestContext = {
|
||||
const test = ava.serial as TestFn<TestContext>;
|
||||
|
||||
let safeFetchStub: Sinon.SinonStub | undefined;
|
||||
let originalDeploymentType: typeof env.DEPLOYMENT_TYPE;
|
||||
let safeFetchHandler:
|
||||
| ((request: { url: string; method?: 'get' | 'head' }) => {
|
||||
status?: number;
|
||||
@@ -38,6 +39,7 @@ const stubSafeFetch = (
|
||||
};
|
||||
|
||||
test.before(async t => {
|
||||
originalDeploymentType = env.DEPLOYMENT_TYPE;
|
||||
// @ts-expect-error test
|
||||
env.DEPLOYMENT_TYPE = 'selfhosted';
|
||||
|
||||
@@ -73,6 +75,8 @@ test.afterEach.always(() => {
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
// @ts-expect-error test
|
||||
env.DEPLOYMENT_TYPE = originalDeploymentType;
|
||||
safeFetchStub?.restore();
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import test from 'ava';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CANARY_CLIENT_VERSION_MAX_AGE_DAYS } from '../../../base';
|
||||
import { Flavor } from '../../../env';
|
||||
import { PublicDocMode } from '../../../models';
|
||||
import { CopilotEmbeddingRealtimeProvider } from '../../../plugins/copilot/context';
|
||||
@@ -63,6 +64,10 @@ const user: CurrentUser = {
|
||||
emailVerified: true,
|
||||
};
|
||||
|
||||
function makeCanaryDateVersion(date: Date, build = '015') {
|
||||
return `${date.getUTCFullYear()}.${date.getUTCMonth() + 1}.${date.getUTCDate()}-canary.${build}`;
|
||||
}
|
||||
|
||||
function createGateway(registry: RealtimeRegistry) {
|
||||
return new RealtimeGateway(registry, {
|
||||
attachServer() {},
|
||||
@@ -142,6 +147,73 @@ test('gateway handles registered request with version gate', async t => {
|
||||
);
|
||||
});
|
||||
|
||||
test('gateway accepts canary date client version in canary namespace', async t => {
|
||||
const originalNamespace = env.NAMESPACE;
|
||||
// @ts-expect-error test
|
||||
env.NAMESPACE = 'dev';
|
||||
try {
|
||||
const registry = new RealtimeRegistry();
|
||||
registry.registerRequest({
|
||||
name: 'notification.count.get',
|
||||
input: z.object({}).strict(),
|
||||
handle: async () => ({ count: 1 }),
|
||||
});
|
||||
const gateway = createGateway(registry);
|
||||
|
||||
t.deepEqual(
|
||||
await gateway.onRequest(user, {
|
||||
op: 'notification.count.get',
|
||||
input: {},
|
||||
clientVersion: makeCanaryDateVersion(new Date(), '040'),
|
||||
}),
|
||||
{ data: { count: 1 } }
|
||||
);
|
||||
|
||||
const old = new Date(
|
||||
Date.now() -
|
||||
(CANARY_CLIENT_VERSION_MAX_AGE_DAYS + 1) * 24 * 60 * 60 * 1000
|
||||
);
|
||||
t.like(
|
||||
await gateway.onRequest(user, {
|
||||
op: 'notification.count.get',
|
||||
input: {},
|
||||
clientVersion: makeCanaryDateVersion(old, '040'),
|
||||
}),
|
||||
{ error: { code: 'UNSUPPORTED_CLIENT_VERSION' } }
|
||||
);
|
||||
} finally {
|
||||
// @ts-expect-error test
|
||||
env.NAMESPACE = originalNamespace;
|
||||
}
|
||||
});
|
||||
|
||||
test('gateway rejects canary date client version outside canary namespace', async t => {
|
||||
const originalNamespace = env.NAMESPACE;
|
||||
// @ts-expect-error test
|
||||
env.NAMESPACE = 'production';
|
||||
try {
|
||||
const registry = new RealtimeRegistry();
|
||||
registry.registerRequest({
|
||||
name: 'notification.count.get',
|
||||
input: z.object({}).strict(),
|
||||
handle: async () => ({ count: 1 }),
|
||||
});
|
||||
const gateway = createGateway(registry);
|
||||
|
||||
t.like(
|
||||
await gateway.onRequest(user, {
|
||||
op: 'notification.count.get',
|
||||
input: {},
|
||||
clientVersion: makeCanaryDateVersion(new Date(), '40'),
|
||||
}),
|
||||
{ error: { code: 'UNSUPPORTED_CLIENT_VERSION' } }
|
||||
);
|
||||
} finally {
|
||||
// @ts-expect-error test
|
||||
env.NAMESPACE = originalNamespace;
|
||||
}
|
||||
});
|
||||
|
||||
test('gateway authorizes subscription and joins room', async t => {
|
||||
const registry = new RealtimeRegistry();
|
||||
registry.registerTopic({
|
||||
|
||||
@@ -19,6 +19,7 @@ import semver from 'semver';
|
||||
import type { Server, Socket } from 'socket.io';
|
||||
|
||||
import {
|
||||
checkCanaryDateClientVersion,
|
||||
GatewayErrorWrapper,
|
||||
OnEvent,
|
||||
UnsupportedClientVersion,
|
||||
@@ -35,6 +36,19 @@ const MIN_REALTIME_CLIENT_VERSION = new semver.Range('>=0.26.0-0', {
|
||||
includePrerelease: true,
|
||||
});
|
||||
|
||||
function normalizeRealtimeClientVersion(clientVersion: string): string | null {
|
||||
const canaryCheck = checkCanaryDateClientVersion(clientVersion);
|
||||
if (!canaryCheck.matched) {
|
||||
return clientVersion;
|
||||
}
|
||||
|
||||
if (!env.namespaces.canary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return canaryCheck.allowed ? canaryCheck.normalized : null;
|
||||
}
|
||||
|
||||
@WebSocketGateway()
|
||||
@UseInterceptors(ClsInterceptor)
|
||||
export class RealtimeGateway implements OnGatewayInit, OnGatewayDisconnect {
|
||||
@@ -122,10 +136,13 @@ export class RealtimeGateway implements OnGatewayInit, OnGatewayDisconnect {
|
||||
}
|
||||
|
||||
private assertVersion(clientVersion?: string) {
|
||||
const normalized = clientVersion
|
||||
? normalizeRealtimeClientVersion(clientVersion)
|
||||
: null;
|
||||
if (
|
||||
!clientVersion ||
|
||||
!semver.valid(clientVersion) ||
|
||||
!MIN_REALTIME_CLIENT_VERSION.test(clientVersion)
|
||||
!normalized ||
|
||||
!semver.valid(normalized) ||
|
||||
!MIN_REALTIME_CLIENT_VERSION.test(normalized)
|
||||
) {
|
||||
throw new UnsupportedClientVersion({
|
||||
clientVersion: clientVersion ?? 'unset_or_invalid',
|
||||
|
||||
@@ -85,14 +85,16 @@ type SyncProtocolRoomType = Extract<RoomType, 'sync-025' | 'sync-026'>;
|
||||
const SOCKET_PRESENCE_USER_ID_KEY = 'affinePresenceUserId';
|
||||
|
||||
function normalizeWsClientVersion(clientVersion: string): string | null {
|
||||
if (env.namespaces.canary) {
|
||||
const canaryCheck = checkCanaryDateClientVersion(clientVersion);
|
||||
if (canaryCheck.matched) {
|
||||
return canaryCheck.allowed ? canaryCheck.normalized : null;
|
||||
}
|
||||
const canaryCheck = checkCanaryDateClientVersion(clientVersion);
|
||||
if (!canaryCheck.matched) {
|
||||
return clientVersion;
|
||||
}
|
||||
|
||||
return clientVersion;
|
||||
if (!env.namespaces.canary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return canaryCheck.allowed ? canaryCheck.normalized : null;
|
||||
}
|
||||
|
||||
function isSupportedWsClientVersion(clientVersion: string): boolean {
|
||||
|
||||
@@ -186,6 +186,9 @@ class AdminDashboardInput {
|
||||
|
||||
@Field(() => Int, { nullable: true, defaultValue: 28 })
|
||||
sharedLinkWindowDays?: number;
|
||||
|
||||
@Field(() => Int, { nullable: true, defaultValue: 7 })
|
||||
copilotWindowDays?: number;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
@@ -250,6 +253,9 @@ class AdminDashboard {
|
||||
@Field(() => SafeIntResolver)
|
||||
copilotConversations!: number;
|
||||
|
||||
@Field(() => TimeWindow)
|
||||
copilotWindow!: TimeWindow;
|
||||
|
||||
@Field(() => SafeIntResolver)
|
||||
workspaceStorageBytes!: number;
|
||||
|
||||
@@ -532,6 +538,7 @@ export class AdminWorkspaceResolver {
|
||||
storageHistoryDays: input?.storageHistoryDays,
|
||||
syncHistoryHours: input?.syncHistoryHours,
|
||||
sharedLinkWindowDays: input?.sharedLinkWindowDays,
|
||||
copilotWindowDays: input?.copilotWindowDays,
|
||||
includeTopSharedLinks,
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { BaseModel } from './base';
|
||||
const DEFAULT_STORAGE_HISTORY_DAYS = 30;
|
||||
const DEFAULT_SYNC_HISTORY_HOURS = 48;
|
||||
const DEFAULT_SHARED_LINK_WINDOW_DAYS = 28;
|
||||
const DEFAULT_COPILOT_WINDOW_DAYS = 7;
|
||||
const DEFAULT_ANALYTICS_WINDOW_DAYS = 28;
|
||||
const NON_TEAM_ANALYTICS_WINDOW_DAYS = 7;
|
||||
const DEFAULT_TIMEZONE = 'UTC';
|
||||
@@ -50,6 +51,7 @@ export type AdminDashboardOptions = {
|
||||
storageHistoryDays?: number;
|
||||
syncHistoryHours?: number;
|
||||
sharedLinkWindowDays?: number;
|
||||
copilotWindowDays?: number;
|
||||
includeTopSharedLinks?: boolean;
|
||||
};
|
||||
|
||||
@@ -99,6 +101,7 @@ export type AdminDashboardDto = {
|
||||
}>;
|
||||
syncWindow: TimeWindowDto;
|
||||
copilotConversations: number;
|
||||
copilotWindow: TimeWindowDto;
|
||||
workspaceStorageBytes: number;
|
||||
blobStorageBytes: number;
|
||||
workspaceStorageHistory: Array<{
|
||||
@@ -262,6 +265,12 @@ export class WorkspaceAnalyticsModel extends BaseModel {
|
||||
90,
|
||||
DEFAULT_SHARED_LINK_WINDOW_DAYS
|
||||
);
|
||||
const copilotWindowDays = clampInt(
|
||||
options.copilotWindowDays,
|
||||
1,
|
||||
90,
|
||||
DEFAULT_COPILOT_WINDOW_DAYS
|
||||
);
|
||||
const includeTopSharedLinks = options.includeTopSharedLinks ?? true;
|
||||
|
||||
const now = new Date();
|
||||
@@ -274,6 +283,7 @@ export class WorkspaceAnalyticsModel extends BaseModel {
|
||||
const currentDay = startOfUtcDay(now);
|
||||
const storageFrom = addUtcDays(currentDay, -(storageHistoryDays - 1));
|
||||
const sharedFrom = addUtcDays(currentDay, -(sharedLinkWindowDays - 1));
|
||||
const copilotFrom = addUtcDays(currentDay, -(copilotWindowDays - 1));
|
||||
|
||||
const topSharedLinksPromise = includeTopSharedLinks
|
||||
? this.db.$queryRaw<
|
||||
@@ -432,7 +442,7 @@ export class WorkspaceAnalyticsModel extends BaseModel {
|
||||
SELECT COUNT(*) AS conversations
|
||||
FROM ai_sessions_messages
|
||||
WHERE role = 'user'
|
||||
AND created_at >= ${sharedFrom}
|
||||
AND created_at >= ${copilotFrom}
|
||||
AND created_at <= ${now}
|
||||
`,
|
||||
topSharedLinksPromise,
|
||||
@@ -497,6 +507,14 @@ export class WorkspaceAnalyticsModel extends BaseModel {
|
||||
effectiveSize: syncHistoryHours,
|
||||
},
|
||||
copilotConversations: Number(copilotCount[0]?.conversations ?? 0),
|
||||
copilotWindow: {
|
||||
from: copilotFrom,
|
||||
to: now,
|
||||
timezone,
|
||||
bucket: 'Day',
|
||||
requestedSize: options.copilotWindowDays ?? DEFAULT_COPILOT_WINDOW_DAYS,
|
||||
effectiveSize: copilotWindowDays,
|
||||
},
|
||||
workspaceStorageBytes: currentWorkspaceStorageBytes,
|
||||
blobStorageBytes: currentBlobStorageBytes,
|
||||
workspaceStorageHistory: storageHistorySeries.map(row => ({
|
||||
|
||||
@@ -63,6 +63,7 @@ type AdminDashboard {
|
||||
blobStorageBytes: SafeInt!
|
||||
blobStorageHistory: [AdminDashboardValueDayPoint!]!
|
||||
copilotConversations: SafeInt!
|
||||
copilotWindow: TimeWindow!
|
||||
generatedAt: DateTime!
|
||||
storageWindow: TimeWindow!
|
||||
syncActiveUsers: Int!
|
||||
@@ -75,6 +76,7 @@ type AdminDashboard {
|
||||
}
|
||||
|
||||
input AdminDashboardInput {
|
||||
copilotWindowDays: Int = 7
|
||||
sharedLinkWindowDays: Int = 28
|
||||
storageHistoryDays: Int = 30
|
||||
syncHistoryHours: Int = 48
|
||||
|
||||
Reference in New Issue
Block a user