feat(core): improve dashboard style (#15205)

This commit is contained in:
DarkSky
2026-07-07 12:13:11 +08:00
committed by GitHub
parent 631e4af9b9
commit fe2a4db76b
18 changed files with 502 additions and 217 deletions
@@ -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 => ({
+2
View File
@@ -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