Reduce server console polling load
This commit is contained in:
@@ -2441,6 +2441,7 @@ func (h *coreHandlers) jobs(w http.ResponseWriter, r *http.Request) {
|
|||||||
ServerInstanceID: r.URL.Query().Get("serverInstanceId"),
|
ServerInstanceID: r.URL.Query().Get("serverInstanceId"),
|
||||||
RunEndpointID: r.URL.Query().Get("runEndpointId"),
|
RunEndpointID: r.URL.Query().Get("runEndpointId"),
|
||||||
State: domain.JobState(r.URL.Query().Get("state")),
|
State: domain.JobState(r.URL.Query().Get("state")),
|
||||||
|
States: parseJobStates(r.URL.Query().Get("states")),
|
||||||
}
|
}
|
||||||
var jobs []domain.Job
|
var jobs []domain.Job
|
||||||
var err error
|
var err error
|
||||||
@@ -2471,6 +2472,20 @@ func (h *coreHandlers) jobs(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseJobStates(raw string) []domain.JobState {
|
||||||
|
if strings.TrimSpace(raw) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
states := []domain.JobState{}
|
||||||
|
for _, part := range strings.Split(raw, ",") {
|
||||||
|
state := domain.JobState(strings.TrimSpace(part))
|
||||||
|
if state != "" {
|
||||||
|
states = append(states, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return states
|
||||||
|
}
|
||||||
|
|
||||||
// jobCancel godoc
|
// jobCancel godoc
|
||||||
// @Summary Request job cancellation
|
// @Summary Request job cancellation
|
||||||
// @Description Records a cancellation request for an accepted or running job; run observes it through the job cancel poll route.
|
// @Description Records a cancellation request for an accepted or running job; run observes it through the job cancel poll route.
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ func TestCoreAPICreateListDetailWorkflows(t *testing.T) {
|
|||||||
getJSON[dto.JobResponse](t, router, "/api/v1/jobs/job-1")
|
getJSON[dto.JobResponse](t, router, "/api/v1/jobs/job-1")
|
||||||
jobs := getJSON[dto.JobListResponse](t, router, "/api/v1/jobs?serverInstanceId=server-1&runEndpointId=run-local&state=queued")
|
jobs := getJSON[dto.JobListResponse](t, router, "/api/v1/jobs?serverInstanceId=server-1&runEndpointId=run-local&state=queued")
|
||||||
assertListCount(t, jobs.Count, 1)
|
assertListCount(t, jobs.Count, 1)
|
||||||
|
jobs = getJSON[dto.JobListResponse](t, router, "/api/v1/jobs?serverInstanceId=server-1&states=queued,running,failed")
|
||||||
|
assertListCount(t, jobs.Count, 1)
|
||||||
|
jobs = getJSON[dto.JobListResponse](t, router, "/api/v1/jobs?serverInstanceId=server-1&states=running,failed")
|
||||||
|
assertListCount(t, jobs.Count, 0)
|
||||||
|
|
||||||
artifactResponse := postJSON[dto.ArtifactResponse](t, router, "/api/v1/artifacts", dto.ArtifactCreateRequest{
|
artifactResponse := postJSON[dto.ArtifactResponse](t, router, "/api/v1/artifacts", dto.ArtifactCreateRequest{
|
||||||
ID: "artifact-1",
|
ID: "artifact-1",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ Plugin-owned data is an independent, server-scoped plugin store. It is not a pro
|
|||||||
- `GET /api/v1/metrics/server-instances`
|
- `GET /api/v1/metrics/server-instances`
|
||||||
- `GET /api/v1/run/endpoints?status=online`
|
- `GET /api/v1/run/endpoints?status=online`
|
||||||
- `GET /api/v1/jobs?serverInstanceId=server-1&runEndpointId=run-local&state=queued`
|
- `GET /api/v1/jobs?serverInstanceId=server-1&runEndpointId=run-local&state=queued`
|
||||||
|
- `GET /api/v1/jobs?serverInstanceId=server-1&states=queued,running,failed`
|
||||||
- `GET /api/v1/artifacts?ownerKind=job&ownerId=job-1&state=uploading`
|
- `GET /api/v1/artifacts?ownerKind=job&ownerId=job-1&state=uploading`
|
||||||
- `GET /api/v1/log-streams?serverInstanceId=server-1&streamKey=stdout`
|
- `GET /api/v1/log-streams?serverInstanceId=server-1&streamKey=stdout`
|
||||||
|
|
||||||
|
|||||||
@@ -1596,6 +1596,7 @@ type JobFilter struct {
|
|||||||
ServerInstanceID string
|
ServerInstanceID string
|
||||||
RunEndpointID string
|
RunEndpointID string
|
||||||
State JobState
|
State JobState
|
||||||
|
States []JobState
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArtifactFilter struct {
|
type ArtifactFilter struct {
|
||||||
|
|||||||
@@ -544,7 +544,22 @@ func matchRunEndpoint(endpoint domain.RunEndpoint, filter domain.RunEndpointFilt
|
|||||||
func matchJob(job domain.Job, filter domain.JobFilter) bool {
|
func matchJob(job domain.Job, filter domain.JobFilter) bool {
|
||||||
return (filter.ServerInstanceID == "" || job.ServerInstanceID == filter.ServerInstanceID) &&
|
return (filter.ServerInstanceID == "" || job.ServerInstanceID == filter.ServerInstanceID) &&
|
||||||
(filter.RunEndpointID == "" || job.RunEndpointID == filter.RunEndpointID) &&
|
(filter.RunEndpointID == "" || job.RunEndpointID == filter.RunEndpointID) &&
|
||||||
(filter.State == "" || job.State == filter.State)
|
matchJobState(job.State, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchJobState(state domain.JobState, filter domain.JobFilter) bool {
|
||||||
|
if filter.State != "" && state != filter.State {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(filter.States) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, candidate := range filter.States {
|
||||||
|
if state == candidate {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchArtifact(artifact domain.Artifact, filter domain.ArtifactFilter) bool {
|
func matchArtifact(artifact domain.Artifact, filter domain.ArtifactFilter) bool {
|
||||||
|
|||||||
@@ -311,12 +311,21 @@ describe("PlatformApiClient AI providers", () => {
|
|||||||
if (url.endsWith("/api/v1/run/endpoints")) {
|
if (url.endsWith("/api/v1/run/endpoints")) {
|
||||||
return jsonResponse({ items: [endpoint], count: 1 });
|
return jsonResponse({ items: [endpoint], count: 1 });
|
||||||
}
|
}
|
||||||
|
if (url.endsWith("/api/v1/run/endpoints?status=online")) {
|
||||||
|
return jsonResponse({ items: [endpoint], count: 1 });
|
||||||
|
}
|
||||||
if (url.endsWith("/api/v1/jobs")) {
|
if (url.endsWith("/api/v1/jobs")) {
|
||||||
return jsonResponse({ items: [job], count: 1 });
|
return jsonResponse({ items: [job], count: 1 });
|
||||||
}
|
}
|
||||||
if (url.endsWith("/api/v1/jobs?serverInstanceId=server-1")) {
|
if (url.endsWith("/api/v1/jobs?serverInstanceId=server-1")) {
|
||||||
return jsonResponse({ items: [job], count: 1 });
|
return jsonResponse({ items: [job], count: 1 });
|
||||||
}
|
}
|
||||||
|
if (url.endsWith("/api/v1/jobs?states=queued%2Crunning%2Cfailed")) {
|
||||||
|
return jsonResponse({ items: [job], count: 1 });
|
||||||
|
}
|
||||||
|
if (url.endsWith("/api/v1/jobs?serverInstanceId=server-1&states=queued%2Crunning%2Cfailed")) {
|
||||||
|
return jsonResponse({ items: [job], count: 1 });
|
||||||
|
}
|
||||||
if (url.endsWith("/api/v1/artifacts?ownerKind=job&ownerId=job-1&state=available")) {
|
if (url.endsWith("/api/v1/artifacts?ownerKind=job&ownerId=job-1&state=available")) {
|
||||||
return jsonResponse({ items: [artifact], count: 1 });
|
return jsonResponse({ items: [artifact], count: 1 });
|
||||||
}
|
}
|
||||||
@@ -571,8 +580,11 @@ describe("PlatformApiClient AI providers", () => {
|
|||||||
await expect(client.uploadServerFile(server.id, { directoryKey: "configs", file: new File(["server.name=Example\n"], "server.properties", { type: "text/plain" }), idempotencyKey: "idem-file-upload" })).resolves.toMatchObject({ inputRef: "artifact://artifact-upload-1", job: { capability: "files.write" } });
|
await expect(client.uploadServerFile(server.id, { directoryKey: "configs", file: new File(["server.name=Example\n"], "server.properties", { type: "text/plain" }), idempotencyKey: "idem-file-upload" })).resolves.toMatchObject({ inputRef: "artifact://artifact-upload-1", job: { capability: "files.write" } });
|
||||||
await expect(client.prepareServerFileDownload(server.id, { key: "config/server.properties", idempotencyKey: "idem-file-download" })).resolves.toMatchObject({ status: "ready", filename: "server.properties", content: "server.name=Example\n" });
|
await expect(client.prepareServerFileDownload(server.id, { key: "config/server.properties", idempotencyKey: "idem-file-download" })).resolves.toMatchObject({ status: "ready", filename: "server.properties", content: "server.name=Example\n" });
|
||||||
await expect(client.listRunEndpoints()).resolves.toMatchObject({ count: 1 });
|
await expect(client.listRunEndpoints()).resolves.toMatchObject({ count: 1 });
|
||||||
|
await expect(client.listRunEndpoints({ status: "online" })).resolves.toMatchObject({ count: 1 });
|
||||||
await expect(client.listJobs()).resolves.toMatchObject({ count: 1 });
|
await expect(client.listJobs()).resolves.toMatchObject({ count: 1 });
|
||||||
await expect(client.listJobs(server.id)).resolves.toMatchObject({ count: 1 });
|
await expect(client.listJobs(server.id)).resolves.toMatchObject({ count: 1 });
|
||||||
|
await expect(client.listJobs(undefined, { states: ["queued", "running", "failed"] })).resolves.toMatchObject({ count: 1 });
|
||||||
|
await expect(client.listJobs(server.id, { states: ["queued", "running", "failed"] })).resolves.toMatchObject({ count: 1 });
|
||||||
await expect(client.listArtifacts({ ownerKind: "job", ownerId: job.id, state: "available" })).resolves.toMatchObject({ count: 1, items: [{ id: artifact.id }] });
|
await expect(client.listArtifacts({ ownerKind: "job", ownerId: job.id, state: "available" })).resolves.toMatchObject({ count: 1, items: [{ id: artifact.id }] });
|
||||||
await expect(client.openArtifactDownload(artifact.id)).resolves.toMatchObject({ downloadUrl: "/api/v1/artifacts/artifact-1/content", rangeSupported: true });
|
await expect(client.openArtifactDownload(artifact.id)).resolves.toMatchObject({ downloadUrl: "/api/v1/artifacts/artifact-1/content", rangeSupported: true });
|
||||||
await expect(client.readArtifactContent(artifact.id, 0, 8)).resolves.toMatchObject({ contentLength: 8, contentRange: "bytes 0-7/18", checksum: artifact.checksum });
|
await expect(client.readArtifactContent(artifact.id, 0, 8)).resolves.toMatchObject({ contentLength: 8, contentRange: "bytes 0-7/18", checksum: artifact.checksum });
|
||||||
@@ -613,7 +625,7 @@ describe("PlatformApiClient AI providers", () => {
|
|||||||
client.invokeAI({ requestId: "ai-1", serverInstanceId: server.id, purpose: "config.suggest", prompt: "Tune PVP safely", currentConfig: "server.name=Example Survival #1\n" })
|
client.invokeAI({ requestId: "ai-1", serverInstanceId: server.id, purpose: "config.suggest", prompt: "Tune PVP safely", currentConfig: "server.name=Example Survival #1\n" })
|
||||||
).resolves.toMatchObject({ status: "ok", usage: { mocked: true }, configRecommendation: { diffSummary: "review required" } });
|
).resolves.toMatchObject({ status: "ok", usage: { mocked: true }, configRecommendation: { diffSummary: "review required" } });
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(43);
|
expect(fetchMock).toHaveBeenCalledTimes(46);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes server file workspace null arrays from older platform responses", async () => {
|
it("normalizes server file workspace null arrays from older platform responses", async () => {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import type {
|
|||||||
JobCreateRequest,
|
JobCreateRequest,
|
||||||
JobListResponse,
|
JobListResponse,
|
||||||
JobResponse,
|
JobResponse,
|
||||||
|
JobState,
|
||||||
LlmConfigSuggestionRequest,
|
LlmConfigSuggestionRequest,
|
||||||
LlmConfigSuggestionResponse,
|
LlmConfigSuggestionResponse,
|
||||||
LogStreamCursorRequest,
|
LogStreamCursorRequest,
|
||||||
@@ -59,6 +60,7 @@ import type {
|
|||||||
RunDistributionGenerateRequest,
|
RunDistributionGenerateRequest,
|
||||||
RunDistributionResponse,
|
RunDistributionResponse,
|
||||||
RunEndpointListResponse,
|
RunEndpointListResponse,
|
||||||
|
RunEndpointStatus,
|
||||||
RunUpdateJobResponse,
|
RunUpdateJobResponse,
|
||||||
RunUpdateJobListResponse,
|
RunUpdateJobListResponse,
|
||||||
RunUpdateRequest,
|
RunUpdateRequest,
|
||||||
@@ -142,6 +144,16 @@ export interface PlatformEventStream {
|
|||||||
close(): void;
|
close(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RunEndpointListFilter {
|
||||||
|
status?: RunEndpointStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JobListFilter {
|
||||||
|
runEndpointId?: string;
|
||||||
|
state?: JobState;
|
||||||
|
states?: JobState[];
|
||||||
|
}
|
||||||
|
|
||||||
export class PlatformApiClient {
|
export class PlatformApiClient {
|
||||||
constructor(private readonly baseUrl = "/api/v1", private readonly sessionTokenProvider: () => string | null = () => platformApiSessionToken) {}
|
constructor(private readonly baseUrl = "/api/v1", private readonly sessionTokenProvider: () => string | null = () => platformApiSessionToken) {}
|
||||||
|
|
||||||
@@ -241,13 +253,12 @@ export class PlatformApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async listRunEndpoints(): Promise<RunEndpointListResponse> {
|
async listRunEndpoints(filter: RunEndpointListFilter = {}): Promise<RunEndpointListResponse> {
|
||||||
return this.request<RunEndpointListResponse>("/run/endpoints");
|
return this.request<RunEndpointListResponse>(`/run/endpoints${runEndpointQuery(filter)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listJobs(serverInstanceId?: string): Promise<JobListResponse> {
|
async listJobs(serverInstanceId?: string, filter: JobListFilter = {}): Promise<JobListResponse> {
|
||||||
const params = serverInstanceId ? `?serverInstanceId=${encodeURIComponent(serverInstanceId)}` : "";
|
return this.request<JobListResponse>(`/jobs${jobListQuery(serverInstanceId, filter)}`);
|
||||||
return this.request<JobListResponse>(`/jobs${params}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async listArtifacts(filter: ArtifactFilterRequest = {}): Promise<ArtifactListResponse> {
|
async listArtifacts(filter: ArtifactFilterRequest = {}): Promise<ArtifactListResponse> {
|
||||||
@@ -901,6 +912,23 @@ function artifactQuery(filter: ArtifactFilterRequest): string {
|
|||||||
return query ? `?${query}` : "";
|
return query ? `?${query}` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runEndpointQuery(filter: RunEndpointListFilter): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter.status) params.set("status", filter.status);
|
||||||
|
const query = params.toString();
|
||||||
|
return query ? `?${query}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function jobListQuery(serverInstanceId: string | undefined, filter: JobListFilter): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (serverInstanceId) params.set("serverInstanceId", serverInstanceId);
|
||||||
|
if (filter.runEndpointId) params.set("runEndpointId", filter.runEndpointId);
|
||||||
|
if (filter.state) params.set("state", filter.state);
|
||||||
|
if (filter.states?.length) params.set("states", filter.states.join(","));
|
||||||
|
const query = params.toString();
|
||||||
|
return query ? `?${query}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
function serverFileListQuery(request: Partial<ServerFileListRequest>): string {
|
function serverFileListQuery(request: Partial<ServerFileListRequest>): string {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (request.directoryKey) params.set("directoryKey", request.directoryKey);
|
if (request.directoryKey) params.set("directoryKey", request.directoryKey);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import { PluginPageHostPage } from "./PluginPageHostPage";
|
|||||||
|
|
||||||
type LoadState<T> = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T };
|
type LoadState<T> = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T };
|
||||||
|
|
||||||
const serverDetailRefreshMs = 5000;
|
const serverDetailRefreshMs = 15000;
|
||||||
|
|
||||||
export function ServerDetailPage(props: PageComponentProps) {
|
export function ServerDetailPage(props: PageComponentProps) {
|
||||||
const { session, params, operations, onNavigate } = props;
|
const { session, params, operations, onNavigate } = props;
|
||||||
@@ -46,7 +46,6 @@ export function ServerDetailPage(props: PageComponentProps) {
|
|||||||
const [section, setSection] = useState<ServerDetailSection>("manage");
|
const [section, setSection] = useState<ServerDetailSection>("manage");
|
||||||
const [instance, setInstance] = useState<LoadState<ServerInstanceResponse>>({ status: "loading" });
|
const [instance, setInstance] = useState<LoadState<ServerInstanceResponse>>({ status: "loading" });
|
||||||
const [plugins, setPlugins] = useState<GamePluginResponse[]>([]);
|
const [plugins, setPlugins] = useState<GamePluginResponse[]>([]);
|
||||||
const [jobs, setJobs] = useState<JobResponse[]>([]);
|
|
||||||
const [runEndpoint, setRunEndpoint] = useState<RunEndpointResponse | undefined>();
|
const [runEndpoint, setRunEndpoint] = useState<RunEndpointResponse | undefined>();
|
||||||
const [deployment, setDeployment] = useState<LoadState<ServerDeploymentResponse>>({ status: "loading" });
|
const [deployment, setDeployment] = useState<LoadState<ServerDeploymentResponse>>({ status: "loading" });
|
||||||
const [confirm, setConfirm] = useState<null | { title: string; description: string; danger?: boolean; run: () => Promise<void> }>(null);
|
const [confirm, setConfirm] = useState<null | { title: string; description: string; danger?: boolean; run: () => Promise<void> }>(null);
|
||||||
@@ -61,19 +60,17 @@ export function ServerDetailPage(props: PageComponentProps) {
|
|||||||
}
|
}
|
||||||
setInstance({ status: "loading" });
|
setInstance({ status: "loading" });
|
||||||
try {
|
try {
|
||||||
const [detail, pluginResponse, jobResponse, deploymentResponse, endpointResponse] = await Promise.all([
|
const [detail, pluginResponse, deploymentResponse, endpointResponse] = await Promise.all([
|
||||||
platformApiClient.getServerInstance(serverId),
|
platformApiClient.getServerInstance(serverId),
|
||||||
platformApiClient.listGamePlugins(),
|
platformApiClient.listGamePlugins(),
|
||||||
platformApiClient.listJobs(serverId),
|
|
||||||
platformApiClient
|
platformApiClient
|
||||||
.getServerDeployment(serverId)
|
.getServerDeployment(serverId)
|
||||||
.then((data): LoadState<ServerDeploymentResponse> => ({ status: "ready", data }))
|
.then((data): LoadState<ServerDeploymentResponse> => ({ status: "ready", data }))
|
||||||
.catch((error): LoadState<ServerDeploymentResponse> => ({ status: "error", reason: error instanceof Error ? error.message : "部署定义加载失败" })),
|
.catch((error): LoadState<ServerDeploymentResponse> => ({ status: "error", reason: error instanceof Error ? error.message : "部署定义加载失败" })),
|
||||||
platformApiClient.listRunEndpoints().catch(() => ({ items: [], count: 0 }))
|
platformApiClient.listRunEndpoints({ status: "online" }).catch(() => ({ items: [], count: 0 }))
|
||||||
]);
|
]);
|
||||||
setInstance({ status: "ready", data: detail });
|
setInstance({ status: "ready", data: detail });
|
||||||
setPlugins(pluginResponse.items);
|
setPlugins(pluginResponse.items);
|
||||||
setJobs(jobResponse.items);
|
|
||||||
setDeployment(deploymentResponse);
|
setDeployment(deploymentResponse);
|
||||||
setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId));
|
setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -90,13 +87,11 @@ export function ServerDetailPage(props: PageComponentProps) {
|
|||||||
const refreshOperationalState = useCallback(async () => {
|
const refreshOperationalState = useCallback(async () => {
|
||||||
if (!serverId) return;
|
if (!serverId) return;
|
||||||
try {
|
try {
|
||||||
const [detail, jobResponse, endpointResponse] = await Promise.all([
|
const [detail, endpointResponse] = await Promise.all([
|
||||||
platformApiClient.getServerInstance(serverId),
|
platformApiClient.getServerInstance(serverId),
|
||||||
platformApiClient.listJobs(serverId),
|
platformApiClient.listRunEndpoints({ status: "online" })
|
||||||
platformApiClient.listRunEndpoints()
|
|
||||||
]);
|
]);
|
||||||
setInstance({ status: "ready", data: detail });
|
setInstance({ status: "ready", data: detail });
|
||||||
setJobs(jobResponse.items);
|
|
||||||
setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId));
|
setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId));
|
||||||
} catch {
|
} catch {
|
||||||
setRunEndpoint(undefined);
|
setRunEndpoint(undefined);
|
||||||
|
|||||||
@@ -62,9 +62,10 @@ const statusFilters: Array<{ id: ServerStatusFilter; label: string }> = [
|
|||||||
{ id: "attention", label: "需关注" }
|
{ id: "attention", label: "需关注" }
|
||||||
];
|
];
|
||||||
|
|
||||||
const serverListRefreshMs = 5000;
|
const serverListRefreshMs = 15000;
|
||||||
const serverMetricFreshMs = 30000;
|
const serverMetricFreshMs = 30000;
|
||||||
const serverForceDeleteConfirmation = "FORCE DELETE";
|
const serverForceDeleteConfirmation = "FORCE DELETE";
|
||||||
|
const serverOperationalJobStates: JobResponse["state"][] = ["queued", "accepted", "running", "retrying", "failed"];
|
||||||
|
|
||||||
export function ServersPage({ session, operations, onNavigate }: PageComponentProps) {
|
export function ServersPage({ session, operations, onNavigate }: PageComponentProps) {
|
||||||
const [listState, setListState] = useState<ListState>("loading");
|
const [listState, setListState] = useState<ListState>("loading");
|
||||||
@@ -93,16 +94,16 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
|||||||
if (showLoading) setListState("loading");
|
if (showLoading) setListState("loading");
|
||||||
try {
|
try {
|
||||||
const [pluginResponse, endpointResponse, instanceResponse, jobResponse] = await Promise.all([
|
const [pluginResponse, endpointResponse, instanceResponse, jobResponse] = await Promise.all([
|
||||||
platformApiClient.listGamePlugins(),
|
showLoading ? platformApiClient.listGamePlugins() : Promise.resolve(undefined),
|
||||||
platformApiClient.listRunEndpoints(),
|
platformApiClient.listRunEndpoints({ status: "online" }),
|
||||||
platformApiClient.listServerInstances(),
|
platformApiClient.listServerInstances(),
|
||||||
platformApiClient.listJobs()
|
platformApiClient.listJobs(undefined, { states: serverOperationalJobStates })
|
||||||
]);
|
]);
|
||||||
setPlugins(pluginResponse.items);
|
if (pluginResponse) setPlugins(pluginResponse.items);
|
||||||
setEndpoints(endpointResponse.items);
|
setEndpoints(endpointResponse.items);
|
||||||
setInstances(instanceResponse.items);
|
setInstances(instanceResponse.items);
|
||||||
setJobs(jobResponse.items);
|
setJobs(jobResponse.items);
|
||||||
if (showLoading) setForm((current) => {
|
if (showLoading && pluginResponse) setForm((current) => {
|
||||||
const plugin = pluginResponse.items.find((item) => item.id === current.pluginId) ?? pluginResponse.items[0];
|
const plugin = pluginResponse.items.find((item) => item.id === current.pluginId) ?? pluginResponse.items[0];
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
@@ -144,10 +145,14 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
void refreshList(false);
|
void refreshList(false);
|
||||||
void refreshMetrics();
|
|
||||||
}, serverListRefreshMs);
|
}, serverListRefreshMs);
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, [refreshList, refreshMetrics]);
|
}, [refreshList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setInterval(() => void refreshMetrics(), serverMetricFreshMs);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
}, [refreshMetrics]);
|
||||||
|
|
||||||
const cards = useMemo<ServerCardView[]>(
|
const cards = useMemo<ServerCardView[]>(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { defaultThemeBackgroundId, defaultThemePaletteId, loadThemeState } from
|
|||||||
|
|
||||||
const sessionTokenStorageKey = "platform-web.session.apiToken";
|
const sessionTokenStorageKey = "platform-web.session.apiToken";
|
||||||
const localFallbackEnabled = readWebRuntimeEnv().enableLocalAuthFallback;
|
const localFallbackEnabled = readWebRuntimeEnv().enableLocalAuthFallback;
|
||||||
|
let currentUserLoadPromise: Promise<CurrentUserView | null> | null = null;
|
||||||
|
|
||||||
export const localFallbackUser: CurrentUserView = {
|
export const localFallbackUser: CurrentUserView = {
|
||||||
id: "local-server-operator",
|
id: "local-server-operator",
|
||||||
@@ -57,6 +58,14 @@ export interface SessionState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadCurrentUser(): Promise<CurrentUserView | null> {
|
export async function loadCurrentUser(): Promise<CurrentUserView | null> {
|
||||||
|
if (currentUserLoadPromise) return currentUserLoadPromise;
|
||||||
|
currentUserLoadPromise = loadCurrentUserOnce().finally(() => {
|
||||||
|
currentUserLoadPromise = null;
|
||||||
|
});
|
||||||
|
return currentUserLoadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCurrentUserOnce(): Promise<CurrentUserView | null> {
|
||||||
const storedToken = readStoredSessionToken();
|
const storedToken = readStoredSessionToken();
|
||||||
setPlatformApiSessionToken(storedToken);
|
setPlatformApiSessionToken(storedToken);
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user