feat(console-ui): manage active deployments
- load deployment state with the console snapshot - mark externally routed revisions in service tables - add guarded activation confirmation and pending states - cover deployment API wiring in SSR tests
This commit is contained in:
+22
-2
@@ -27,7 +27,7 @@ export type BackendService = {
|
||||
export type BackendEvent = {
|
||||
id: number;
|
||||
timestamp_ms: number;
|
||||
kind: "registered" | "started" | "stopped" | "invoked" | "failed";
|
||||
kind: "registered" | "deployed" | "started" | "stopped" | "invoked" | "failed";
|
||||
service_id: string | null;
|
||||
revision: string | null;
|
||||
message: string;
|
||||
@@ -41,6 +41,13 @@ export type BackendRuntime = {
|
||||
running_services: number;
|
||||
};
|
||||
|
||||
export type BackendDeployment = {
|
||||
service_id: string;
|
||||
active_revision: string;
|
||||
status: BackendServiceStatus;
|
||||
updated_at_ms: number;
|
||||
};
|
||||
|
||||
export type BackendWitDependency = {
|
||||
name: string;
|
||||
version: string;
|
||||
@@ -80,20 +87,33 @@ export function saveApiBase(value: string): string {
|
||||
}
|
||||
|
||||
export async function fetchSnapshot(apiBase: string) {
|
||||
const [services, events, runtime, witPackages] = await Promise.all([
|
||||
const [services, deployments, events, runtime, witPackages] = await Promise.all([
|
||||
request<{ services: BackendService[] }>(apiBase, "/api/v1/services"),
|
||||
request<{ deployments: BackendDeployment[] }>(apiBase, "/api/v1/deployments"),
|
||||
request<{ events: BackendEvent[] }>(apiBase, "/api/v1/events"),
|
||||
request<BackendRuntime>(apiBase, "/api/v1/runtime"),
|
||||
request<{ packages: BackendWitPackage[] }>(apiBase, "/api/v1/wit/packages"),
|
||||
]);
|
||||
return {
|
||||
services: services.services,
|
||||
deployments: deployments.deployments,
|
||||
events: events.events,
|
||||
runtime,
|
||||
witPackages: witPackages.packages,
|
||||
};
|
||||
}
|
||||
|
||||
export async function activateDeployment(
|
||||
apiBase: string,
|
||||
service: Pick<BackendService, "id" | "revision">,
|
||||
): Promise<BackendDeployment> {
|
||||
return request(apiBase, `/api/v1/deployments/${encodeURIComponent(service.id)}/activate`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ revision: service.revision }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function registerComponent(
|
||||
apiBase: string,
|
||||
input: RegisterComponentInput,
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
Play,
|
||||
RadioTower,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Search,
|
||||
@@ -31,11 +32,13 @@ import {
|
||||
import { FormEvent, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
BackendEvent,
|
||||
BackendDeployment,
|
||||
BackendRuntime,
|
||||
BackendService,
|
||||
BackendWitPackage,
|
||||
InvokeResult,
|
||||
RegisterComponentInput,
|
||||
activateDeployment,
|
||||
changeRuntimeState,
|
||||
changeServiceState,
|
||||
fetchSnapshot,
|
||||
@@ -66,6 +69,7 @@ type Service = {
|
||||
calls: number;
|
||||
errors: number;
|
||||
latencyMs: number | null;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
type RuntimeEvent = {
|
||||
@@ -101,7 +105,7 @@ function serviceKey(service: Pick<Service, "id" | "revision">) {
|
||||
return `${service.id}@${service.revision}`;
|
||||
}
|
||||
|
||||
function toService(service: BackendService): Service {
|
||||
function toService(service: BackendService, activeRevision?: string): Service {
|
||||
return {
|
||||
id: service.id,
|
||||
revision: service.revision,
|
||||
@@ -119,6 +123,7 @@ function toService(service: BackendService): Service {
|
||||
calls: service.calls,
|
||||
errors: service.errors,
|
||||
latencyMs: service.last_latency_ms,
|
||||
active: service.revision === activeRevision,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,6 +132,7 @@ function toEvent(event: BackendEvent): RuntimeEvent {
|
||||
event.service_id && event.revision ? `${event.service_id}@${event.revision}` : "Wasmeld";
|
||||
const meta: Record<BackendEvent["kind"], { label: string; tone: EventTone }> = {
|
||||
registered: { label: "已注册", tone: "success" },
|
||||
deployed: { label: "部署切换", tone: "success" },
|
||||
started: { label: "已启动", tone: "success" },
|
||||
stopped: { label: "已停止", tone: "neutral" },
|
||||
invoked: { label: "调用完成", tone: "success" },
|
||||
@@ -202,6 +208,7 @@ function Home() {
|
||||
const [view, setView] = useState<View>("overview");
|
||||
const [services, setServices] = useState<Service[]>([]);
|
||||
const [events, setEvents] = useState<RuntimeEvent[]>([]);
|
||||
const [deployments, setDeployments] = useState<BackendDeployment[]>([]);
|
||||
const [witPackages, setWitPackages] = useState<BackendWitPackage[]>([]);
|
||||
const [runtime, setRuntime] = useState<BackendRuntime | null>(null);
|
||||
const [runtimeAction, setRuntimeAction] = useState<"start" | "stop" | "restart" | null>(null);
|
||||
@@ -213,11 +220,15 @@ function Home() {
|
||||
const [deployOpen, setDeployOpen] = useState(false);
|
||||
const [witPublishOpen, setWitPublishOpen] = useState(false);
|
||||
const [invokeKey, setInvokeKey] = useState<string | null>(null);
|
||||
const [activationKey, setActivationKey] = useState<string | null>(null);
|
||||
const [deploymentAction, setDeploymentAction] = useState<string | null>(null);
|
||||
const [toast, setToast] = useState<string | null>(null);
|
||||
|
||||
const selectedService =
|
||||
services.find((service) => serviceKey(service) === selectedKey) ?? services[0] ?? null;
|
||||
const invokeService = services.find((service) => serviceKey(service) === invokeKey) ?? null;
|
||||
const activationService =
|
||||
services.find((service) => serviceKey(service) === activationKey) ?? null;
|
||||
|
||||
const filteredServices = useMemo(() => {
|
||||
const normalized = query.trim().toLowerCase();
|
||||
@@ -250,8 +261,17 @@ function Home() {
|
||||
const refreshSnapshot = useCallback(async () => {
|
||||
try {
|
||||
const snapshot = await fetchSnapshot(apiBase);
|
||||
const nextServices = snapshot.services.map(toService);
|
||||
const activeRevisions = new Map(
|
||||
snapshot.deployments.map((deployment) => [
|
||||
deployment.service_id,
|
||||
deployment.active_revision,
|
||||
]),
|
||||
);
|
||||
const nextServices = snapshot.services.map((service) =>
|
||||
toService(service, activeRevisions.get(service.id)),
|
||||
);
|
||||
setServices(nextServices);
|
||||
setDeployments(snapshot.deployments);
|
||||
setEvents(snapshot.events.map(toEvent));
|
||||
setRuntime(snapshot.runtime);
|
||||
setWitPackages(snapshot.witPackages);
|
||||
@@ -265,6 +285,7 @@ function Home() {
|
||||
setConnection("online");
|
||||
} catch {
|
||||
setServices([]);
|
||||
setDeployments([]);
|
||||
setEvents([]);
|
||||
setRuntime(null);
|
||||
setWitPackages([]);
|
||||
@@ -318,6 +339,21 @@ function Home() {
|
||||
}
|
||||
}
|
||||
|
||||
async function activateService(service: Service) {
|
||||
const key = serviceKey(service);
|
||||
setDeploymentAction(key);
|
||||
try {
|
||||
await activateDeployment(apiBase, service);
|
||||
await refreshSnapshot();
|
||||
setActivationKey(null);
|
||||
notify(`${service.id}@${service.revision} 已设为对外版本`);
|
||||
} catch (error) {
|
||||
notify(error instanceof Error ? error.message : "Deployment 切换失败");
|
||||
} finally {
|
||||
setDeploymentAction(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function runInvocation(service: Service, input: Uint8Array) {
|
||||
const result = await invokeComponent(apiBase, service, input);
|
||||
await refreshSnapshot();
|
||||
@@ -405,16 +441,19 @@ function Home() {
|
||||
running={running}
|
||||
faulted={faulted}
|
||||
totalCalls={totalCalls}
|
||||
deploymentCount={deployments.length}
|
||||
runtime={runtime}
|
||||
selectedService={selectedService}
|
||||
onSelect={(service) => setSelectedKey(serviceKey(service))}
|
||||
onDeploy={() => setDeployOpen(true)}
|
||||
onInvoke={(service) => setInvokeKey(serviceKey(service))}
|
||||
onStatus={setServiceStatus}
|
||||
onActivate={(service) => setActivationKey(serviceKey(service))}
|
||||
onViewAll={() => setView("services")}
|
||||
onRefresh={() => void refreshSnapshot()}
|
||||
onRuntimeAction={(action) => void setRuntimeStatus(action)}
|
||||
runtimeAction={runtimeAction}
|
||||
deploymentAction={deploymentAction}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -427,6 +466,9 @@ function Home() {
|
||||
onInvoke={(service) => setInvokeKey(serviceKey(service))}
|
||||
onSelect={(service) => setSelectedKey(serviceKey(service))}
|
||||
onStatus={setServiceStatus}
|
||||
onActivate={(service) => setActivationKey(serviceKey(service))}
|
||||
canActivate={runtime?.status === "running"}
|
||||
deploymentAction={deploymentAction}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -484,6 +526,19 @@ function Home() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{activationService && (
|
||||
<ActivateDeploymentDialog
|
||||
service={activationService}
|
||||
currentRevision={
|
||||
services.find((service) => service.id === activationService.id && service.active)
|
||||
?.revision ?? null
|
||||
}
|
||||
submitting={deploymentAction === serviceKey(activationService)}
|
||||
onClose={() => setActivationKey(null)}
|
||||
onConfirm={() => void activateService(activationService)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{toast && (
|
||||
<output className="toast">
|
||||
<CheckCircle2 size={17} />
|
||||
@@ -613,16 +668,19 @@ function Overview({
|
||||
running,
|
||||
faulted,
|
||||
totalCalls,
|
||||
deploymentCount,
|
||||
runtime,
|
||||
selectedService,
|
||||
onSelect,
|
||||
onDeploy,
|
||||
onInvoke,
|
||||
onStatus,
|
||||
onActivate,
|
||||
onViewAll,
|
||||
onRefresh,
|
||||
onRuntimeAction,
|
||||
runtimeAction,
|
||||
deploymentAction,
|
||||
}: {
|
||||
services: Service[];
|
||||
filteredServices: Service[];
|
||||
@@ -630,16 +688,19 @@ function Overview({
|
||||
running: number;
|
||||
faulted: number;
|
||||
totalCalls: number;
|
||||
deploymentCount: number;
|
||||
runtime: BackendRuntime | null;
|
||||
selectedService: Service | null;
|
||||
onSelect: (service: Service) => void;
|
||||
onDeploy: () => void;
|
||||
onInvoke: (service: Service) => void;
|
||||
onStatus: (service: Service, status: ServiceStatus) => void;
|
||||
onActivate: (service: Service) => void;
|
||||
onViewAll: () => void;
|
||||
onRefresh: () => void;
|
||||
onRuntimeAction: (action: "start" | "stop" | "restart") => void;
|
||||
runtimeAction: "start" | "stop" | "restart" | null;
|
||||
deploymentAction: string | null;
|
||||
}) {
|
||||
const errorCount = services.reduce((sum, service) => sum + service.errors, 0);
|
||||
const latencies = services
|
||||
@@ -677,7 +738,7 @@ function Overview({
|
||||
icon={<Package size={18} />}
|
||||
label="服务版本"
|
||||
value={services.length.toString()}
|
||||
note="来自 Wasmeld"
|
||||
note={`${deploymentCount} 个对外服务`}
|
||||
tone="cyan"
|
||||
/>
|
||||
<Metric
|
||||
@@ -794,6 +855,9 @@ function Overview({
|
||||
onSelect={onSelect}
|
||||
onInvoke={onInvoke}
|
||||
onStatus={onStatus}
|
||||
onActivate={onActivate}
|
||||
canActivate={runtime?.status === "running"}
|
||||
deploymentAction={deploymentAction}
|
||||
compact
|
||||
/>
|
||||
</section>
|
||||
@@ -832,6 +896,17 @@ function Overview({
|
||||
<DetailValue label="Deadline" value={`${selectedService.deadlineMs} ms`} />
|
||||
<DetailValue label="Mailbox" value={selectedService.mailbox.toString()} />
|
||||
<div className="detail-actions">
|
||||
{!selectedService.active && (
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
disabled={runtime?.status !== "running" || deploymentAction !== null}
|
||||
onClick={() => onActivate(selectedService)}
|
||||
>
|
||||
<RadioTower size={16} />
|
||||
设为对外
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
@@ -908,6 +983,9 @@ function ServiceTable({
|
||||
onSelect,
|
||||
onInvoke,
|
||||
onStatus,
|
||||
onActivate,
|
||||
canActivate,
|
||||
deploymentAction,
|
||||
compact = false,
|
||||
}: {
|
||||
services: Service[];
|
||||
@@ -915,6 +993,9 @@ function ServiceTable({
|
||||
onSelect: (service: Service) => void;
|
||||
onInvoke: (service: Service) => void;
|
||||
onStatus: (service: Service, status: ServiceStatus) => void;
|
||||
onActivate: (service: Service) => void;
|
||||
canActivate: boolean;
|
||||
deploymentAction: string | null;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
if (services.length === 0) {
|
||||
@@ -959,7 +1040,15 @@ function ServiceTable({
|
||||
<Box size={16} />
|
||||
</span>
|
||||
<span>
|
||||
<strong>{service.id}</strong>
|
||||
<span className="service-title">
|
||||
<strong>{service.id}</strong>
|
||||
{service.active && (
|
||||
<span className="active-deployment-badge">
|
||||
<RadioTower size={10} />
|
||||
对外
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<small>
|
||||
{service.revision} · {service.description}
|
||||
</small>
|
||||
@@ -976,6 +1065,30 @@ function ServiceTable({
|
||||
</td>
|
||||
<td>
|
||||
<div className="row-actions">
|
||||
<button
|
||||
className={service.active ? "active-deployment-button" : ""}
|
||||
type="button"
|
||||
aria-label={
|
||||
service.active
|
||||
? `${service.id}@${service.revision} 是当前对外版本`
|
||||
: `将 ${service.id}@${service.revision} 设为对外版本`
|
||||
}
|
||||
title={
|
||||
service.active
|
||||
? "当前对外版本"
|
||||
: canActivate
|
||||
? "设为对外版本"
|
||||
: "请先启动 Runtime"
|
||||
}
|
||||
disabled={service.active || !canActivate || deploymentAction !== null}
|
||||
onClick={() => onActivate(service)}
|
||||
>
|
||||
{deploymentAction === serviceKey(service) ? (
|
||||
<RefreshCw className="spin" size={15} />
|
||||
) : (
|
||||
<RadioTower size={15} />
|
||||
)}
|
||||
</button>
|
||||
{service.status === "running" ? (
|
||||
<>
|
||||
<button
|
||||
@@ -1055,6 +1168,9 @@ function ServicesView({
|
||||
onInvoke,
|
||||
onSelect,
|
||||
onStatus,
|
||||
onActivate,
|
||||
canActivate,
|
||||
deploymentAction,
|
||||
}: {
|
||||
services: Service[];
|
||||
statusFilter: "all" | ServiceStatus;
|
||||
@@ -1063,13 +1179,16 @@ function ServicesView({
|
||||
onInvoke: (service: Service) => void;
|
||||
onSelect: (service: Service) => void;
|
||||
onStatus: (service: Service, status: ServiceStatus) => void;
|
||||
onActivate: (service: Service) => void;
|
||||
canActivate: boolean;
|
||||
deploymentAction: string | null;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
eyebrow="REGISTRY"
|
||||
title="服务版本"
|
||||
description="管理 Runtime 已注册的不可变 Wasm Component 制品。"
|
||||
description="管理不可变 Wasm Component 制品及当前对外版本。"
|
||||
actions={
|
||||
<button className="primary-button" type="button" onClick={onDeploy}>
|
||||
<CloudUpload size={17} />
|
||||
@@ -1103,6 +1222,9 @@ function ServicesView({
|
||||
onSelect={onSelect}
|
||||
onInvoke={onInvoke}
|
||||
onStatus={onStatus}
|
||||
onActivate={onActivate}
|
||||
canActivate={canActivate}
|
||||
deploymentAction={deploymentAction}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
@@ -1435,12 +1557,14 @@ function DialogFrame({
|
||||
description,
|
||||
icon,
|
||||
onClose,
|
||||
closeDisabled = false,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
onClose: () => void;
|
||||
closeDisabled?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
@@ -1449,9 +1573,12 @@ function DialogFrame({
|
||||
open
|
||||
className="dialog"
|
||||
aria-label={title}
|
||||
onCancel={onClose}
|
||||
onCancel={(event) => {
|
||||
if (closeDisabled) event.preventDefault();
|
||||
else onClose();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") onClose();
|
||||
if (event.key === "Escape" && !closeDisabled) onClose();
|
||||
}}
|
||||
>
|
||||
<div className="dialog-header">
|
||||
@@ -1467,6 +1594,7 @@ function DialogFrame({
|
||||
type="button"
|
||||
aria-label="关闭"
|
||||
title="关闭"
|
||||
disabled={closeDisabled}
|
||||
onClick={onClose}
|
||||
>
|
||||
<X size={18} />
|
||||
@@ -1478,6 +1606,56 @@ function DialogFrame({
|
||||
);
|
||||
}
|
||||
|
||||
function ActivateDeploymentDialog({
|
||||
service,
|
||||
currentRevision,
|
||||
submitting,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: {
|
||||
service: Service;
|
||||
currentRevision: string | null;
|
||||
submitting: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
return (
|
||||
<DialogFrame
|
||||
title="切换对外版本"
|
||||
description="更新该服务在 Gateway 上接收请求的活动版本。"
|
||||
icon={<RadioTower size={19} />}
|
||||
onClose={onClose}
|
||||
closeDisabled={submitting}
|
||||
>
|
||||
<div className="deployment-confirmation">
|
||||
<div className="deployment-route">
|
||||
<div>
|
||||
<span>当前版本</span>
|
||||
<strong>{currentRevision ?? "尚未部署"}</strong>
|
||||
</div>
|
||||
<ArrowRight size={18} />
|
||||
<div className="deployment-target">
|
||||
<span>目标版本</span>
|
||||
<strong>{service.revision}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Wasmeld 将先确认 {service.id}@{service.revision} 的 Actor 可用,再更新对外路由。
|
||||
</p>
|
||||
</div>
|
||||
<div className="dialog-footer">
|
||||
<button className="secondary-button" type="button" disabled={submitting} onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
<button className="primary-button" type="button" disabled={submitting} onClick={onConfirm}>
|
||||
{submitting ? <RefreshCw className="spin" size={16} /> : <RadioTower size={16} />}
|
||||
{submitting ? "正在切换" : "确认切换"}
|
||||
</button>
|
||||
</div>
|
||||
</DialogFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function DeployDialog({
|
||||
onClose,
|
||||
onRegister,
|
||||
|
||||
@@ -785,7 +785,7 @@ button:disabled {
|
||||
}
|
||||
|
||||
.data-table th:nth-last-child(1) {
|
||||
width: 122px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
@@ -849,6 +849,33 @@ button:disabled {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.service-title {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.service-title strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.active-deployment-badge {
|
||||
flex: 0 0 auto;
|
||||
height: 17px;
|
||||
padding: 0 5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: #176b4d;
|
||||
background: #e4f3ed;
|
||||
border: 1px solid #c5e4d8;
|
||||
border-radius: 9px;
|
||||
font-size: 7px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.service-cell small {
|
||||
margin-top: 3px;
|
||||
color: var(--ink-faint);
|
||||
@@ -917,7 +944,7 @@ button:disabled {
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
min-width: 100px;
|
||||
min-width: 128px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
@@ -943,6 +970,14 @@ button:disabled {
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
.row-actions .active-deployment-button:disabled {
|
||||
color: #197253;
|
||||
background: #e7f4ef;
|
||||
border-color: #c6e4d9;
|
||||
cursor: default;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.event-list {
|
||||
padding: 4px 14px;
|
||||
}
|
||||
@@ -1094,6 +1129,7 @@ button:disabled {
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.full-panel {
|
||||
@@ -1597,6 +1633,54 @@ button:disabled {
|
||||
margin: 18px -18px -18px;
|
||||
}
|
||||
|
||||
.deployment-confirmation {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.deployment-route {
|
||||
min-height: 72px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 24px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.deployment-route > svg {
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
|
||||
.deployment-route span,
|
||||
.deployment-route strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.deployment-route span {
|
||||
margin-bottom: 5px;
|
||||
color: var(--ink-faint);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.deployment-route strong {
|
||||
overflow: hidden;
|
||||
font-family: var(--font-geist-mono), monospace;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.deployment-target {
|
||||
color: #176b4d;
|
||||
}
|
||||
|
||||
.deployment-confirmation p {
|
||||
margin: 13px 0 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 9px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.invoke-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@@ -77,15 +77,16 @@ test("server-renders the Wasm management console", async () => {
|
||||
assert.match(html, /运行概览/);
|
||||
assert.match(html, /WIT 包/);
|
||||
assert.match(html, /连接中/);
|
||||
assert.match(html, /来自 Wasmeld/);
|
||||
assert.match(html, /0 个对外服务/);
|
||||
assert.match(html, /Component Model/);
|
||||
assert.doesNotMatch(html, /本地演示|codex-preview|vinext|next\.js/i);
|
||||
});
|
||||
|
||||
test("uses TanStack Start routing and produces Node artifacts", async () => {
|
||||
const [rootRoute, indexRoute, router, packageJson, viteConfig] = await Promise.all([
|
||||
const [rootRoute, indexRoute, apiClient, router, packageJson, viteConfig] = await Promise.all([
|
||||
readFile(new URL("../src/routes/__root.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../src/routes/index.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../src/api.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../src/router.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../package.json", import.meta.url), "utf8"),
|
||||
readFile(new URL("../vite.config.ts", import.meta.url), "utf8"),
|
||||
@@ -95,6 +96,8 @@ test("uses TanStack Start routing and produces Node artifacts", async () => {
|
||||
assert.match(rootRoute, /<HeadContent \/>/);
|
||||
assert.match(rootRoute, /<Scripts \/>/);
|
||||
assert.match(indexRoute, /createFileRoute\("\/"\)/);
|
||||
assert.match(indexRoute, /切换对外版本/);
|
||||
assert.match(apiClient, /\/api\/v1\/deployments/);
|
||||
assert.match(router, /createRouter/);
|
||||
assert.match(packageJson, /"@tanstack\/react-start"/);
|
||||
assert.match(packageJson, /"srvx"/);
|
||||
|
||||
Reference in New Issue
Block a user