Files
AgentHub/client/src/components/settings/AutostartSettings.tsx
T

169 lines
5.5 KiB
TypeScript

/**
* @file AutostartSettings — Settings → About's "Start automatically" switch:
* whether the dashboard starts at boot/login through the per-user service
* (systemd `--user` unit, or a LaunchAgent on macOS), over
* `GET/PUT /api/settings/autostart`. Off by default; a change never starts or
* stops the running server. Beside it, **Quit AgentHub** (issue #65, 9router's
* tray Quit): `POST /api/settings/quit` closes every workspace terminal and
* stops the server, which stays stopped until `agenthub start` — or the next
* boot/login while the switch is on. Between them, the status-area icon switch
* (`GET/PUT /api/settings/tray`, on by default — server/lib/tray.js). Last,
* **Uninstall AgentHub**: `POST /api/settings/uninstall` removes AgentHub from
* the machine, database included (scripts/uninstall.js). Quit is a plain
* button and Uninstall a danger one, both the same width with an icon, and
* each spins while its request runs.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Loader2, Power, Trash2 } from "lucide-react";
import { api, type AutostartStatus, type TrayStatus } from "../../lib/api";
import { toast } from "../../lib/toast";
import { confirmDialog } from "../../lib/confirm";
import { SettingsRow, SettingsSection, SettingsSwitchRow } from "./SettingsRow";
// The two action buttons share one width, so they line up under the switches.
const ACTION = "min-w-[6.5rem] justify-center text-xs";
const ICON = "h-3.5 w-3.5 shrink-0";
export function AutostartSettings() {
const { t } = useTranslation("settings");
const [status, setStatus] = useState<AutostartStatus | null>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
api.settings.autostart
.get()
.then(setStatus)
.catch(() => {});
}, []);
const change = async (enabled: boolean) => {
setSaving(true);
try {
setStatus(await api.settings.autostart.set(enabled));
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
} finally {
setSaving(false);
}
};
const [tray, setTray] = useState<TrayStatus | null>(null);
useEffect(() => {
api.settings.tray
.get()
.then(setTray)
.catch(() => {});
}, []);
const changeTray = async (enabled: boolean) => {
try {
setTray(await api.settings.tray.set(enabled));
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
};
const [quitting, setQuitting] = useState(false);
const quit = async () => {
const ok = await confirmDialog({
title: t("quit.confirmTitle"),
message: t(status?.enabled ? "quit.confirmMessageAutostart" : "quit.confirmMessage"),
confirmLabel: t("quit.button"),
});
if (!ok) return;
setQuitting(true);
try {
await api.settings.quit();
toast.success(t("quit.done"));
} catch (err) {
setQuitting(false);
toast.error(err instanceof Error ? err.message : String(err));
}
};
const [uninstalling, setUninstalling] = useState(false);
const uninstall = async () => {
const ok = await confirmDialog({
title: t("uninstall.confirmTitle"),
message: t("uninstall.confirmMessage"),
confirmLabel: t("uninstall.button"),
});
if (!ok) return;
setUninstalling(true);
try {
const { log } = await api.settings.uninstall();
toast.success(t("uninstall.done", { log }));
} catch (err) {
setUninstalling(false);
toast.error(err instanceof Error ? err.message : String(err));
}
};
const note = !status
? undefined
: !status.available
? t("autostart.unavailable")
: status.foreign
? t("autostart.foreign")
: t(`autostart.desc_${status.kind}`);
return (
<SettingsSection id="autostart" title={t("autostart.title")}>
<SettingsSwitchRow
label={t("autostart.label")}
description={note}
checked={!!status?.enabled}
disabled={!status?.available || status.foreign || saving}
onChange={(v) => void change(v)}
/>
<SettingsSwitchRow
label={t("tray.label")}
description={tray && !tray.available ? t("tray.unavailable") : t("tray.desc")}
checked={!!tray?.available && tray.enabled}
disabled={!tray?.available}
onChange={(v) => void changeTray(v)}
/>
<SettingsRow
label={t("quit.label")}
description={t("quit.desc")}
control={
<button
type="button"
data-testid="quit-agenthub"
disabled={quitting}
aria-busy={quitting}
onClick={() => void quit()}
className={`btn-secondary ${ACTION}`}
>
{quitting ? <Loader2 className={ICON + " animate-spin"} /> : <Power className={ICON} />}
{t("quit.button")}
</button>
}
/>
<SettingsRow
label={t("uninstall.label")}
description={t("uninstall.desc")}
control={
<button
type="button"
data-testid="uninstall-agenthub"
disabled={uninstalling}
aria-busy={uninstalling}
onClick={() => void uninstall()}
className={`btn-danger ${ACTION}`}
>
{uninstalling ? (
<Loader2 className={ICON + " animate-spin"} />
) : (
<Trash2 className={ICON} />
)}
{t("uninstall.button")}
</button>
}
/>
</SettingsSection>
);
}