feat(node): package Ubuntu desktop setup and trusted access
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Button, SettingsCard, StatusBadge } from "@nodedc/ui-react";
|
||||
import { APIError, desktopAction, networkSetupAvailable, request } from "./api";
|
||||
|
||||
interface NetworkStatus { installed: boolean; state: string; online: boolean; addresses: string[] }
|
||||
interface NetworkResult { action: string; ok: boolean; error?: string; browser_opened?: boolean }
|
||||
|
||||
const states: Record<string, string> = {
|
||||
not_installed: "Не установлен", unavailable: "Служба недоступна", NeedsLogin: "Требуется вход",
|
||||
NeedsMachineAuth: "Ожидаем разрешения администратора сети", Stopped: "Отключён", Starting: "Подключаемся", NoState: "Запускается",
|
||||
};
|
||||
|
||||
export function TailnetAccess({ failure, revision: hostRevision }: { failure: (error: unknown) => void; revision: string }) {
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [value, setValue] = useState<NetworkStatus | null>(null);
|
||||
const [pending, setPending] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState("");
|
||||
const [revision, setRevision] = useState(0);
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
async function update() {
|
||||
try {
|
||||
const next = await request<NetworkStatus>("/api/network/tailscale");
|
||||
if (active) { setValue(next); if (next.online) setNotice(""); }
|
||||
} catch (error) {
|
||||
if (active) {
|
||||
setValue(null);
|
||||
// Service upgrades invalidate local sessions. Use the existing
|
||||
// application login surface instead of hiding 401 as provider failure.
|
||||
if (error instanceof APIError && error.status === 401) failure(error);
|
||||
}
|
||||
} finally { if (active) { setChecked(true); timer = setTimeout(() => void update(), 5000); } }
|
||||
}
|
||||
void update();
|
||||
return () => { active = false; clearTimeout(timer); };
|
||||
}, [revision, hostRevision, failure]);
|
||||
useEffect(() => {
|
||||
function completed(event: Event) {
|
||||
const result = (event as CustomEvent<NetworkResult>).detail;
|
||||
if (!result || !["install-tailscale", "connect-tailscale"].includes(result.action)) return;
|
||||
setPending(null);
|
||||
if (!result.ok) failure(new Error(result.error ?? "Настройка не завершена. Повторите действие."));
|
||||
setNotice(result.browser_opened ? "Завершите вход в открывшемся браузере. Состояние здесь обновится автоматически." : "");
|
||||
setRevision(value => value + 1);
|
||||
}
|
||||
window.addEventListener("mission-core-network-result", completed);
|
||||
return () => window.removeEventListener("mission-core-network-result", completed);
|
||||
}, [failure]);
|
||||
function perform(action: "install-tailscale" | "connect-tailscale") {
|
||||
if (pending) return;
|
||||
setNotice(""); setPending(action);
|
||||
if (!desktopAction(action)) {
|
||||
setPending(null); failure(new Error("Откройте установленное приложение Mission Core Node из меню Ubuntu."));
|
||||
}
|
||||
}
|
||||
const label = !checked ? "Проверяем подключение" : !value ? "Состояние недоступно" : value.state === "Running" ? value.online ? "В сети" : "Нет связи с координатором" : states[value.state] ?? "Состояние неизвестно";
|
||||
const canConnect = value?.installed && ["NeedsLogin", "Stopped", "unavailable"].includes(value.state);
|
||||
const supported = networkSetupAvailable();
|
||||
return <div className="node-content"><SettingsCard title="Tailscale" description="Частная сеть для удалённого доступа к борту" actions={<StatusBadge tone={value?.online ? "success" : "neutral"}>{label}</StatusBadge>}>
|
||||
<p className="node-note">Частная сеть для доступа к борту с другого компьютера. Войдите в ту же сеть Tailscale, что и на компьютере оператора. При первом подключении настройки локальной сети сохраняются.</p>
|
||||
{!checked ? <ActivityIndicator label="Проверяем подключение Tailscale" /> : !value ? <p className="node-note">Не удалось получить состояние. Повторная проверка выполняется автоматически.</p> : <>
|
||||
{!value.installed && <><p className="node-note">Приложение загрузит проверенный пакет Tailscale и включит его службу. Понадобятся интернет и системное подтверждение Ubuntu.</p><Button disabled={pending !== null || !supported} onClick={() => perform("install-tailscale")}>Установить Tailscale</Button></>}
|
||||
{canConnect && <Button disabled={pending !== null || !supported} onClick={() => perform("connect-tailscale")}>{value.state === "NeedsLogin" ? "Войти в Tailscale" : "Подключить Tailscale"}</Button>}
|
||||
{value.state === "NeedsMachineAuth" && <p className="node-note">Администратор вашей сети должен разрешить подключение этого компьютера в Tailscale.</p>}
|
||||
{value.addresses.length > 0 && <dl className="node-facts"><div><dt>Адреса в Tailscale</dt><dd>{value.addresses.join(" · ")}</dd></div></dl>}
|
||||
</>}
|
||||
{!supported && <p className="node-note">Для настройки сети закройте окно и заново откройте установленное приложение из меню Ubuntu. После обновления пакета требуется перезапуск окна.</p>}
|
||||
{pending && <ActivityIndicator label={pending === "install-tailscale" ? "Подтвердите установку в системном окне Ubuntu. Загружаем и устанавливаем компонент…" : "Подтвердите действие в системном окне Ubuntu. Проверяем подключение…"} />}
|
||||
{notice && <p className="node-note" role="status">{notice}</p>}
|
||||
</SettingsCard></div>;
|
||||
}
|
||||
Reference in New Issue
Block a user