Merge pull request 'GAZ-6: 整合 Stage 1 成果并完成主线验收' (#5) from agent/docker/78eaa92c into main
Reviewed-on: #5
This commit is contained in:
commit
d54841fdb4
10
src/App.tsx
10
src/App.tsx
@ -1,6 +1,8 @@
|
|||||||
import { useReducer, useState } from "react";
|
import { useReducer, useState } from "react";
|
||||||
import { SessionQueryIntent, SessionTable } from "./SessionTable";
|
import { SessionQueryIntent, SessionTable } from "./SessionTable";
|
||||||
|
import { demoSessions } from "./protocol/fixtures";
|
||||||
import { initializeSessionTableStates, updateSessionTableState } from "./sessionTableState";
|
import { initializeSessionTableStates, updateSessionTableState } from "./sessionTableState";
|
||||||
|
import { toSessionRow } from "./sessionPresentation";
|
||||||
import {
|
import {
|
||||||
createInitialWorkspaceState,
|
createInitialWorkspaceState,
|
||||||
selectActiveTab,
|
selectActiveTab,
|
||||||
@ -8,12 +10,8 @@ import {
|
|||||||
workspaceReducer,
|
workspaceReducer,
|
||||||
} from "./workspace";
|
} from "./workspace";
|
||||||
|
|
||||||
const sessions = [
|
const fixtureNow = new Date("2026-08-03T19:12:02Z");
|
||||||
{ id: "NW-042", status: "ONLINE", host: "ws-fin-042", user: "nora.chen", os: "Windows 11", address: "10.42.8.17", seen: "14s ago", tags: ["finance", "priority"] },
|
const sessions = demoSessions.map((session) => toSessionRow(session, fixtureNow));
|
||||||
{ id: "NW-038", status: "ONLINE", host: "srv-app-03", user: "svc.deploy", os: "Ubuntu 24.04", address: "10.42.3.8", seen: "51s ago", tags: ["server"] },
|
|
||||||
{ id: "NW-031", status: "IDLE", host: "mac-design-07", user: "alex.k", os: "macOS 15.6", address: "10.42.12.27", seen: "8m ago", tags: ["design"] },
|
|
||||||
{ id: "NW-019", status: "OFFLINE", host: "ws-ops-019", user: "operator", os: "Windows 10", address: "10.42.6.91", seen: "2h ago", tags: ["legacy"] },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [workspace, dispatch] = useReducer(workspaceReducer, undefined, createInitialWorkspaceState);
|
const [workspace, dispatch] = useReducer(workspaceReducer, undefined, createInitialWorkspaceState);
|
||||||
|
|||||||
@ -1,18 +1,43 @@
|
|||||||
import type { Session } from "../model";
|
import type { CapabilityId, ProjectId, ServerId, Session, SessionId, Timestamp } from "../model";
|
||||||
|
|
||||||
/** UI 迁移期可直接使用的最小 Session fixture。 */
|
const serverId = "atlas" as ServerId;
|
||||||
export const demoSession = {
|
const projectId = "northwind" as ProjectId;
|
||||||
server_id: "demo-server",
|
const overview = "session.overview" as CapabilityId;
|
||||||
project_id: "demo-project",
|
|
||||||
session_id: "demo-session",
|
interface DemoSessionInput {
|
||||||
name: "演示会话",
|
readonly session_id: string;
|
||||||
state: "ONLINE",
|
readonly name: string;
|
||||||
hostname: "demo-host",
|
readonly state: Session["state"];
|
||||||
username: "operator",
|
readonly hostname?: string;
|
||||||
os: "linux",
|
readonly username?: string;
|
||||||
architecture: "amd64",
|
readonly os?: string;
|
||||||
first_seen_at: "2026-08-03T00:00:00Z",
|
readonly architecture?: string;
|
||||||
last_active_at: "2026-08-03T00:00:05Z",
|
readonly source_address?: string;
|
||||||
tags: ["fixture"],
|
readonly first_seen_at: string;
|
||||||
capabilities: ["session.overview"],
|
readonly last_active_at: string;
|
||||||
} as unknown as Session;
|
readonly tags: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function session(
|
||||||
|
value: DemoSessionInput,
|
||||||
|
): Session {
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
server_id: serverId,
|
||||||
|
project_id: projectId,
|
||||||
|
session_id: value.session_id as SessionId,
|
||||||
|
first_seen_at: value.first_seen_at as Timestamp,
|
||||||
|
last_active_at: value.last_active_at as Timestamp,
|
||||||
|
capabilities: [overview],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 界面只通过展示适配器消费的标准协议 Session fixture。 */
|
||||||
|
export const demoSessions: readonly Session[] = [
|
||||||
|
session({ session_id: "NW-042", name: "NW-042", state: "ONLINE", hostname: "ws-fin-042", username: "nora.chen", os: "Windows 11", architecture: "amd64", source_address: "10.42.8.17", first_seen_at: "2026-08-01T08:00:00Z", last_active_at: "2026-08-03T19:11:48Z", tags: ["finance", "priority"] }),
|
||||||
|
session({ session_id: "NW-038", name: "NW-038", state: "ONLINE", hostname: "srv-app-03", username: "svc.deploy", os: "Ubuntu 24.04", architecture: "amd64", source_address: "10.42.3.8", first_seen_at: "2026-07-28T10:30:00Z", last_active_at: "2026-08-03T19:11:11Z", tags: ["server"] }),
|
||||||
|
session({ session_id: "NW-031", name: "NW-031", state: "DORMANT", hostname: "mac-design-07", username: "alex.k", os: "macOS 15.6", architecture: "arm64", source_address: "10.42.12.27", first_seen_at: "2026-07-22T09:15:00Z", last_active_at: "2026-08-03T19:04:00Z", tags: ["design"] }),
|
||||||
|
session({ session_id: "NW-019", name: "NW-019", state: "OFFLINE", hostname: "ws-ops-019", username: "operator", os: "Windows 10", architecture: "amd64", source_address: "10.42.6.91", first_seen_at: "2026-07-10T14:00:00Z", last_active_at: "2026-08-03T17:12:00Z", tags: ["legacy"] }),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const demoSession = demoSessions[0];
|
||||||
|
|||||||
@ -86,6 +86,7 @@ export interface Session extends SessionContext, Extensible {
|
|||||||
readonly username?: string;
|
readonly username?: string;
|
||||||
readonly os?: string;
|
readonly os?: string;
|
||||||
readonly architecture?: string;
|
readonly architecture?: string;
|
||||||
|
readonly source_address?: string;
|
||||||
readonly first_seen_at: Timestamp;
|
readonly first_seen_at: Timestamp;
|
||||||
readonly last_active_at: Timestamp;
|
readonly last_active_at: Timestamp;
|
||||||
readonly tags: readonly string[];
|
readonly tags: readonly string[];
|
||||||
|
|||||||
18
src/sessionPresentation.test.ts
Normal file
18
src/sessionPresentation.test.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { demoSessions } from "./protocol/fixtures";
|
||||||
|
import { toSessionRow } from "./sessionPresentation";
|
||||||
|
|
||||||
|
describe("toSessionRow", () => {
|
||||||
|
it("将协议 Session 转换为稳定的表格展示值", () => {
|
||||||
|
expect(toSessionRow(demoSessions[0], new Date("2026-08-03T19:12:02Z"))).toEqual({
|
||||||
|
id: "NW-042",
|
||||||
|
status: "ONLINE",
|
||||||
|
host: "ws-fin-042",
|
||||||
|
user: "nora.chen",
|
||||||
|
os: "Windows 11 / amd64",
|
||||||
|
address: "10.42.8.17",
|
||||||
|
seen: "14s ago",
|
||||||
|
tags: ["finance", "priority"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
24
src/sessionPresentation.ts
Normal file
24
src/sessionPresentation.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { Session } from "./protocol/model";
|
||||||
|
import type { SessionRow } from "./SessionTable";
|
||||||
|
|
||||||
|
/** 将服务端权威模型转换为表格展示值,不在 UI 复制业务模型。 */
|
||||||
|
export function toSessionRow(session: Session, now: Date = new Date()): SessionRow {
|
||||||
|
return {
|
||||||
|
id: session.session_id,
|
||||||
|
status: session.state,
|
||||||
|
host: session.hostname ?? session.name,
|
||||||
|
user: session.username ?? "—",
|
||||||
|
os: [session.os, session.architecture].filter(Boolean).join(" / ") || "未知",
|
||||||
|
address: session.source_address ?? "—",
|
||||||
|
seen: formatRelativeTime(session.last_active_at, now),
|
||||||
|
tags: [...session.tags],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRelativeTime(timestamp: string, now: Date): string {
|
||||||
|
const elapsedSeconds = Math.max(0, Math.floor((now.getTime() - Date.parse(timestamp)) / 1000));
|
||||||
|
if (elapsedSeconds < 60) return `${elapsedSeconds}s ago`;
|
||||||
|
if (elapsedSeconds < 3600) return `${Math.floor(elapsedSeconds / 60)}m ago`;
|
||||||
|
if (elapsedSeconds < 86400) return `${Math.floor(elapsedSeconds / 3600)}h ago`;
|
||||||
|
return `${Math.floor(elapsedSeconds / 86400)}d ago`;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user