fix: expose server deployment trigger
This commit is contained in:
@@ -115,6 +115,10 @@ export function canStopServer(state: ServerInstanceState): boolean {
|
||||
return state === "running";
|
||||
}
|
||||
|
||||
export function canDeployServer(state: ServerInstanceState): boolean {
|
||||
return state === "draft" || state === "failed";
|
||||
}
|
||||
|
||||
export function isPendingJobState(state: JobResponse["state"]): boolean {
|
||||
return state === "queued" || state === "accepted" || state === "running" || state === "retrying";
|
||||
}
|
||||
|
||||
@@ -162,6 +162,16 @@ describe("ServerDetailPage config write approval", () => {
|
||||
expect(serverDetailPageSource).not.toContain('capability: "process.stop"');
|
||||
});
|
||||
|
||||
it("submits explicit deploy and retry-deploy actions only through the fenced deployment API", () => {
|
||||
expect(serverDetailPageSource).toContain("canDeployServer(instance.data.state)");
|
||||
expect(serverDetailPageSource).toContain("requestDeployment(instance.data)");
|
||||
expect(serverDetailPageSource).toContain("platformApiClient.deployServerInstance(current.id");
|
||||
expect(serverDetailPageSource).toContain('serverLifecycleCommandRequest(current, "deploy")');
|
||||
expect(serverDetailPageSource).toContain('instance.data.state === "failed" ? "重试部署" : "部署"');
|
||||
expect(serverDetailPageSource).toContain('operations.isPending(instance.data.id, instance.data.state === "failed" ? "重试部署服务器" : "部署服务器")');
|
||||
expect(serverDetailPageSource).not.toContain('capability: "process.install"');
|
||||
});
|
||||
|
||||
it("keeps plugin lifecycle and bridge-visible output on platform-owned logical references", () => {
|
||||
expect(serverDetailPageSource).toContain("parsePluginArtifactReference(result)");
|
||||
expect(serverDetailPageSource).toContain("platformApiClient.openArtifactDownload(artifact.id)");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChevronDown, ChevronRight, Download, MoonStar, PackageOpen, Pencil, ShieldCheck, Sparkles, Square, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react";
|
||||
import { ChevronDown, ChevronRight, Download, MoonStar, PackageOpen, Pencil, Rocket, ShieldCheck, Sparkles, Square, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react";
|
||||
import { type FormEvent, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
@@ -50,7 +50,7 @@ import { DiagnosticSummary, EmptyState, ErrorState, LoadingState, ResultBadge }
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
import { jobCapabilityLabel } from "../contracts/jobPresentation";
|
||||
import type { PluginBridgeAction, PluginBridgeManifestContract } from "../contracts/pluginBridge";
|
||||
import { canStartServer, canStopServer, defaultServerCreateForm, endpointLabel, pluginCreateInputDefaults, pluginLabel, runtimeBindingFields, serverMetadataFormFromInstance, type ServerCreateFormState, type ServerMetadataFormState } from "../contracts/serverManagement";
|
||||
import { canDeployServer, canStartServer, canStopServer, defaultServerCreateForm, endpointLabel, pluginCreateInputDefaults, pluginLabel, runtimeBindingFields, serverMetadataFormFromInstance, type ServerCreateFormState, type ServerMetadataFormState } from "../contracts/serverManagement";
|
||||
import {
|
||||
serverDetailSections,
|
||||
serverIsOnline,
|
||||
@@ -212,6 +212,26 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
});
|
||||
}
|
||||
|
||||
function requestDeployment(current: ServerInstanceResponse) {
|
||||
const retry = current.state === "failed";
|
||||
const intent = retry ? "重试部署服务器" : "部署服务器";
|
||||
setConfirm({
|
||||
title: intent,
|
||||
description: `${retry ? "重新" : ""}部署 ${current.name}(${current.id})将向已注册的专属 Run 提交安装任务,确认继续?`,
|
||||
run: async () => {
|
||||
const operationId = operations.begin({ intent, targetKind: "server", targetId: current.id, requester: session.displayName });
|
||||
try {
|
||||
const result = await platformApiClient.deployServerInstance(current.id, serverLifecycleCommandRequest(current, "deploy"));
|
||||
operations.succeed(operationId, `部署任务 ${result.job.id}(${result.job.capability})已派发`, result.job);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
operations.fail(operationId, error instanceof Error ? error.message : "部署任务提交失败", operationId);
|
||||
await refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function saveDeploymentWorkflow(form: ServerCreateFormState) {
|
||||
if (instance.status !== "ready") return;
|
||||
const current = instance.data;
|
||||
@@ -276,6 +296,16 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
</div>
|
||||
<div className="action-strip">
|
||||
<span className={cx("status-pill", statusClass(instance.data.state))}>{stateLabel(instance.data.state)}</span>
|
||||
{canDeployServer(instance.data.state) && <button
|
||||
type="button"
|
||||
className="primary-command"
|
||||
disabled={deployment.status !== "ready" || !deployment.data.mode || operations.isPending(instance.data.id, instance.data.state === "failed" ? "重试部署服务器" : "部署服务器")}
|
||||
title={deployment.status !== "ready" || !deployment.data.mode ? "请先配置部署定义" : undefined}
|
||||
onClick={() => requestDeployment(instance.data)}
|
||||
>
|
||||
<Rocket size={15} />
|
||||
<span>{instance.data.state === "failed" ? "重试部署" : "部署"}</span>
|
||||
</button>}
|
||||
<button
|
||||
type="button"
|
||||
className="icon-command"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { GamePluginResponse } from "../api/types";
|
||||
import { defaultServerCreateForm, runtimeBindingFields } from "../contracts/serverManagement";
|
||||
import { canDeployServer, defaultServerCreateForm, runtimeBindingFields } from "../contracts/serverManagement";
|
||||
import { serverCreateRequestFromForm, serverInstanceIdFromName } from "./serverManagement";
|
||||
|
||||
const plugin: GamePluginResponse = {
|
||||
@@ -42,6 +42,13 @@ const plugin: GamePluginResponse = {
|
||||
};
|
||||
|
||||
describe("runtime profile server creation contracts", () => {
|
||||
it("allows deployment only from draft or failed server states", () => {
|
||||
expect(canDeployServer("draft")).toBe(true);
|
||||
expect(canDeployServer("failed")).toBe(true);
|
||||
expect(canDeployServer("installing")).toBe(false);
|
||||
expect(canDeployServer("ready")).toBe(false);
|
||||
});
|
||||
|
||||
it("derives logical binding fields from the selected profile", () => {
|
||||
expect(runtimeBindingFields(plugin, "local")).toEqual([
|
||||
{ key: "java-runtime", required: false, sensitive: false },
|
||||
|
||||
@@ -52,7 +52,7 @@ export function serverInstanceIdFromName(name: string, sequence = Date.now()): s
|
||||
return normalized ? `server-${normalized}-${suffix}` : `server-${suffix}`;
|
||||
}
|
||||
|
||||
export function serverLifecycleCommandRequest(instance: ServerInstanceResponse, action: "start" | "stop" | "status", sequence = Date.now()): ServerLifecycleCommandRequest {
|
||||
export function serverLifecycleCommandRequest(instance: ServerInstanceResponse, action: "deploy" | "start" | "stop" | "status", sequence = Date.now()): ServerLifecycleCommandRequest {
|
||||
return {
|
||||
expectedConfigVersion: instance.configVersion,
|
||||
expectedChecksum: instance.configChecksum,
|
||||
@@ -130,6 +130,6 @@ export function runtimeIdempotencyKey(action: string, serverInstanceId: string,
|
||||
return `web:${action}:${serverInstanceId}:${sequence}`;
|
||||
}
|
||||
|
||||
export function lifecycleIdempotencyKey(action: "create" | "start" | "stop" | "status", serverInstanceId: string, sequence: number): string {
|
||||
export function lifecycleIdempotencyKey(action: "create" | "deploy" | "start" | "stop" | "status", serverInstanceId: string, sequence: number): string {
|
||||
return `web:${action}:${serverInstanceId}:${sequence}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user