Files
mc-gateway/cmd/gateway/admin_frontend/src/views/services.ts
tursom ecb682bb41
Some checks failed
Docker Image / docker (push) Has been cancelled
Refactor admin frontend build
2026-06-25 21:16:11 +08:00

162 lines
5.5 KiB
TypeScript

import { api } from "../api.js";
import { showAlert } from "../alerts.js";
import { el, escapeAttr, escapeHTML, getFormInput } from "../dom.js";
import { t } from "../i18n.js";
import { isAdmin } from "../session.js";
import { state } from "../state.js";
import type { ServiceRecord } from "../types.js";
interface ServicesResponse {
services?: ServiceRecord[];
}
const serviceNames: Record<string, string> = {
tcp_admin: "TCP/Admin",
kcp: "KCP",
quic: "QUIC",
websocket: "WebSocket",
};
export async function loadServices(): Promise<void> {
try {
const data = await api<ServicesResponse>("/services");
state.services = data.services || [];
renderServices();
} catch (err) {
showAlert((err as Error).message);
}
}
export function renderServices(): void {
el("servicesGrid").innerHTML = state.services.map((service) => `
<article class="service">
<div>
<span>${escapeHTML(formatServiceName(service.name))}</span>
<strong>${serviceStatusText(service)}</strong>
</div>
${isAdmin() ? serviceForm(service) : serviceSummary(service)}
</article>
`).join("");
document.querySelectorAll<HTMLFormElement>("[data-service-form]").forEach((form) => {
form.addEventListener("submit", saveService);
});
document.querySelectorAll<HTMLButtonElement>("[data-restart-service]").forEach((button) => {
button.addEventListener("click", () => {
const name = button.dataset.restartService;
if (name) {
restartService(name);
}
});
});
}
function serviceSummary(service: ServiceRecord): string {
return `<div><span>${escapeHTML(t("port"))}</span><strong>${service.port}</strong></div>`;
}
function serviceForm(service: ServiceRecord): string {
const disabled = service.name === "tcp_admin" ? "disabled" : "";
const optionFields = serviceOptionFields(service);
return `
<form data-service-form="${escapeAttr(service.name)}">
<label class="inline">
<input name="enabled" type="checkbox" ${service.enabled ? "checked" : ""} ${disabled}>
${escapeHTML(t("enabled"))}
</label>
<label>
${escapeHTML(t("port"))}
<input name="port" type="number" min="1" max="65535" value="${service.port}">
</label>
${optionFields}
<div class="row-actions">
<button type="submit">${escapeHTML(t("save"))}</button>
<button class="secondary" type="button" data-restart-service="${escapeAttr(service.name)}">${escapeHTML(t("restart"))}</button>
</div>
</form>
`;
}
function serviceOptionFields(service: ServiceRecord): string {
const options = service.options || {};
if (service.name === "kcp") {
return `
<label>${escapeHTML(t("dataShards"))}<input name="data_shards" type="number" min="1" value="${numberOption(options, "data_shards", 10)}"></label>
<label>${escapeHTML(t("parityShards"))}<input name="parity_shards" type="number" min="1" value="${numberOption(options, "parity_shards", 3)}"></label>
`;
}
if (service.name === "quic") {
const protocols = arrayOption(options, "application_protocols").join(",");
return `<label>${escapeHTML(t("protocols"))}<input name="application_protocols" value="${escapeAttr(protocols)}"></label>`;
}
if (service.name === "websocket") {
return `<label>${escapeHTML(t("path"))}<input name="path" value="${escapeAttr(stringOption(options, "path", "/"))}"></label>`;
}
return "";
}
async function saveService(event: SubmitEvent): Promise<void> {
event.preventDefault();
const form = event.currentTarget as HTMLFormElement;
const name = form.dataset.serviceForm;
if (!name) {
return;
}
const options: Record<string, unknown> = {};
if (name === "kcp") {
options.data_shards = Number(getFormInput(form, "data_shards").value);
options.parity_shards = Number(getFormInput(form, "parity_shards").value);
} else if (name === "quic") {
options.application_protocols = getFormInput(form, "application_protocols").value.split(",").map((item) => item.trim()).filter(Boolean);
} else if (name === "websocket") {
options.path = getFormInput(form, "path").value;
}
try {
await api(`/services/${encodeURIComponent(name)}`, {
method: "PUT",
body: {
enabled: name === "tcp_admin" ? true : getFormInput(form, "enabled").checked,
port: Number(getFormInput(form, "port").value),
options,
},
});
await loadServices();
} catch (err) {
showAlert((err as Error).message);
}
}
async function restartService(name: string): Promise<void> {
try {
await api(`/services/${encodeURIComponent(name)}/restart`, { method: "POST", body: {} });
await loadServices();
} catch (err) {
showAlert((err as Error).message);
}
}
function formatServiceName(name: string): string {
return serviceNames[name] || name;
}
function serviceStatusText(service: ServiceRecord): string {
const status = service.enabled ? t("enabled") : t("disabled");
return escapeHTML(service.restart_required ? `${status} / ${t("restartRequired")}` : status);
}
function numberOption(options: Record<string, unknown>, key: string, fallback: number): number {
const value = options[key];
return typeof value === "number" ? value : fallback;
}
function stringOption(options: Record<string, unknown>, key: string, fallback: string): string {
const value = options[key];
return typeof value === "string" ? value : fallback;
}
function arrayOption(options: Record<string, unknown>, key: string): string[] {
const value = options[key];
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
}