small ahh change But Format
This commit is contained in:
+28
-25
@@ -1,27 +1,30 @@
|
||||
{
|
||||
"name": "wakey-operator-ui",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-compiler-runtime": "^1.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.8"
|
||||
}
|
||||
"name": "wakey-operator-ui",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-compiler-runtime": "^1.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"prettier": "^3.8.2",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.8"
|
||||
}
|
||||
}
|
||||
Generated
+866
-506
File diff suppressed because it is too large
Load Diff
+17
-8
@@ -36,12 +36,13 @@ export function App() {
|
||||
setState("loading");
|
||||
setError("");
|
||||
try {
|
||||
const [nextAgents, nextAlerts, nextHistory, nextAudit] = await Promise.all([
|
||||
fetchAgents(),
|
||||
fetchAlerts(),
|
||||
fetchAlertHistory(20),
|
||||
fetchAudit(30),
|
||||
]);
|
||||
const [nextAgents, nextAlerts, nextHistory, nextAudit] =
|
||||
await Promise.all([
|
||||
fetchAgents(),
|
||||
fetchAlerts(),
|
||||
fetchAlertHistory(20),
|
||||
fetchAudit(30),
|
||||
]);
|
||||
setAgents(nextAgents);
|
||||
setAlerts(nextAlerts);
|
||||
setHistory(nextHistory);
|
||||
@@ -57,7 +58,10 @@ export function App() {
|
||||
}
|
||||
|
||||
async function refreshAlertsAndHistory() {
|
||||
const [nextAlerts, nextHistory] = await Promise.all([fetchAlerts(), fetchAlertHistory(20)]);
|
||||
const [nextAlerts, nextHistory] = await Promise.all([
|
||||
fetchAlerts(),
|
||||
fetchAlertHistory(20),
|
||||
]);
|
||||
setAlerts(nextAlerts);
|
||||
setHistory(nextHistory);
|
||||
}
|
||||
@@ -144,7 +148,12 @@ export function App() {
|
||||
/>
|
||||
<Route
|
||||
path="audit"
|
||||
element={<AuditPage events={audit} onRefresh={() => fetchAudit(30).then(setAudit)} />}
|
||||
element={
|
||||
<AuditPage
|
||||
events={audit}
|
||||
onRefresh={() => fetchAudit(30).then(setAudit)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="alerts"
|
||||
|
||||
+28
-10
@@ -88,42 +88,60 @@ export function fetchAlerts(): Promise<Alert[]> {
|
||||
}
|
||||
|
||||
export function fetchAlertHistory(limit = 50): Promise<AlertTransition[]> {
|
||||
return request<AlertTransition[]>(`/api/v1/control/alerts/history?limit=${limit}`);
|
||||
return request<AlertTransition[]>(
|
||||
`/api/v1/control/alerts/history?limit=${limit}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchAudit(limit = 50): Promise<AuditEvent[]> {
|
||||
return request<AuditEvent[]>(`/api/v1/control/audit/events?limit=${limit}`);
|
||||
}
|
||||
|
||||
export function fetchEnrollTokens(includeExpired = false): Promise<EnrollTokenStatus[]> {
|
||||
export function fetchEnrollTokens(
|
||||
includeExpired = false,
|
||||
): Promise<EnrollTokenStatus[]> {
|
||||
return request<EnrollTokenStatus[]>(
|
||||
`/api/v1/control/enroll-tokens?include_expired=${includeExpired ? "true" : "false"}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function issueEnrollToken(ttlSeconds: number): Promise<IssueEnrollTokenResponse> {
|
||||
export function issueEnrollToken(
|
||||
ttlSeconds: number,
|
||||
): Promise<IssueEnrollTokenResponse> {
|
||||
return request<IssueEnrollTokenResponse>(
|
||||
`/api/v1/control/enroll-token?ttl_seconds=${Math.max(1, Math.floor(ttlSeconds))}`,
|
||||
{ method: "POST" },
|
||||
);
|
||||
}
|
||||
|
||||
export function revokeEnrollToken(token: string): Promise<RevokeEnrollTokenResponse> {
|
||||
export function revokeEnrollToken(
|
||||
token: string,
|
||||
): Promise<RevokeEnrollTokenResponse> {
|
||||
return request<RevokeEnrollTokenResponse>(
|
||||
`/api/v1/control/enroll-tokens/${encodeURIComponent(token)}`,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
}
|
||||
|
||||
export function runCommand(agentId: string, kind: CommandKind, query: string): Promise<unknown> {
|
||||
export function runCommand(
|
||||
agentId: string,
|
||||
kind: CommandKind,
|
||||
query: string,
|
||||
): Promise<unknown> {
|
||||
const payload = buildCommandPayload(kind, query);
|
||||
return request(`/api/v1/control/agents/${encodeURIComponent(agentId)}/command`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return request(
|
||||
`/api/v1/control/agents/${encodeURIComponent(agentId)}/command`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function buildCommandPayload(kind: CommandKind, query: string): { command: Record<string, unknown> } {
|
||||
function buildCommandPayload(
|
||||
kind: CommandKind,
|
||||
query: string,
|
||||
): { command: Record<string, unknown> } {
|
||||
if (kind === "devs") {
|
||||
return { command: { kind: "devs", dev: null, up_only: false } };
|
||||
}
|
||||
|
||||
+40
-19
@@ -28,23 +28,26 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
);
|
||||
|
||||
const filteredAlerts = useMemo(
|
||||
() => alerts.filter((a) => (
|
||||
(severity === "all" || a.severity === severity)
|
||||
&& (status === "all" || a.status === status)
|
||||
&& (kind === "all" || a.kind === kind)
|
||||
)),
|
||||
() =>
|
||||
alerts.filter(
|
||||
(a) =>
|
||||
(severity === "all" || a.severity === severity) &&
|
||||
(status === "all" || a.status === status) &&
|
||||
(kind === "all" || a.kind === kind),
|
||||
),
|
||||
[alerts, severity, status, kind],
|
||||
);
|
||||
|
||||
const filteredTransitions = useMemo(() => {
|
||||
const q = transitionQ.trim().toLowerCase();
|
||||
if (!q) return transitions;
|
||||
return transitions.filter((t) => (
|
||||
t.kind.toLowerCase().includes(q)
|
||||
|| t.to_status.toLowerCase().includes(q)
|
||||
|| t.message.toLowerCase().includes(q)
|
||||
|| (t.agent_id || "").toLowerCase().includes(q)
|
||||
));
|
||||
return transitions.filter(
|
||||
(t) =>
|
||||
t.kind.toLowerCase().includes(q) ||
|
||||
t.to_status.toLowerCase().includes(q) ||
|
||||
t.message.toLowerCase().includes(q) ||
|
||||
(t.agent_id || "").toLowerCase().includes(q),
|
||||
);
|
||||
}, [transitions, transitionQ]);
|
||||
|
||||
return (
|
||||
@@ -57,9 +60,14 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
<div className="grid-3 compact">
|
||||
<label>
|
||||
Severity
|
||||
<select value={severity} onChange={(e) => setSeverity(e.target.value)}>
|
||||
<select
|
||||
value={severity}
|
||||
onChange={(e) => setSeverity(e.target.value)}
|
||||
>
|
||||
{severities.map((v) => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
<option key={v} value={v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
@@ -67,7 +75,9 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
Status
|
||||
<select value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
{statuses.map((v) => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
<option key={v} value={v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
@@ -75,20 +85,29 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
Kind
|
||||
<select value={kind} onChange={(e) => setKind(e.target.value)}>
|
||||
{kinds.map((v) => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
<option key={v} value={v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p className="muted">Showing {filteredAlerts.length} of {alerts.length}</p>
|
||||
<p className="muted">
|
||||
Showing {filteredAlerts.length} of {alerts.length}
|
||||
</p>
|
||||
<div className="list">
|
||||
{filteredAlerts.map((alert) => (
|
||||
<div className="row plain" key={alert.alert_id}>
|
||||
<span>{alert.kind}{alert.agent_id ? ` (${alert.agent_id})` : ""}</span>
|
||||
<span>
|
||||
{alert.kind}
|
||||
{alert.agent_id ? ` (${alert.agent_id})` : ""}
|
||||
</span>
|
||||
<span>{alert.severity}</span>
|
||||
</div>
|
||||
))}
|
||||
{!filteredAlerts.length && <div className="empty">No active alerts</div>}
|
||||
{!filteredAlerts.length && (
|
||||
<div className="empty">No active alerts</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,7 +124,9 @@ export function AlertsPage({ alerts, transitions, onRefresh }: Props) {
|
||||
placeholder="kind, status, message, agent"
|
||||
/>
|
||||
</label>
|
||||
<pre className="output small">{JSON.stringify(filteredTransitions, null, 2)}</pre>
|
||||
<pre className="output small">
|
||||
{JSON.stringify(filteredTransitions, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
+28
-11
@@ -19,7 +19,10 @@ export function AuditPage({ events, onRefresh }: Props) {
|
||||
const [needle, setNeedle] = useState(initialNeedle);
|
||||
|
||||
const eventTypes = useMemo(
|
||||
() => ["all", ...Array.from(new Set(events.map((e) => e.event_type))).sort()],
|
||||
() => [
|
||||
"all",
|
||||
...Array.from(new Set(events.map((e) => e.event_type))).sort(),
|
||||
],
|
||||
[events],
|
||||
);
|
||||
|
||||
@@ -35,11 +38,12 @@ export function AuditPage({ events, onRefresh }: Props) {
|
||||
if (outcome !== "all" && e.outcome !== outcome) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
e.message.toLowerCase().includes(q)
|
||||
|| e.event_type.toLowerCase().includes(q)
|
||||
|| e.outcome.toLowerCase().includes(q)
|
||||
|| (e.actor_id || "").toLowerCase().includes(q)
|
||||
|| (e.agent_id || "").toLowerCase().includes(q)
|
||||
e.message.toLowerCase().includes(q) ||
|
||||
e.event_type.toLowerCase().includes(q) ||
|
||||
e.outcome.toLowerCase().includes(q) ||
|
||||
e.request_id?.toLowerCase().includes(q) || // why does the one flow from devicespage use this
|
||||
(e.actor_id || "").toLowerCase().includes(q) ||
|
||||
(e.agent_id || "").toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}, [events, eventType, outcome, needle]);
|
||||
@@ -53,9 +57,14 @@ export function AuditPage({ events, onRefresh }: Props) {
|
||||
<div className="grid-3 compact">
|
||||
<label>
|
||||
Event type
|
||||
<select value={eventType} onChange={(e) => setEventType(e.target.value)}>
|
||||
<select
|
||||
value={eventType}
|
||||
onChange={(e) => setEventType(e.target.value)}
|
||||
>
|
||||
{eventTypes.map((v) => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
<option key={v} value={v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
@@ -63,16 +72,24 @@ export function AuditPage({ events, onRefresh }: Props) {
|
||||
Outcome
|
||||
<select value={outcome} onChange={(e) => setOutcome(e.target.value)}>
|
||||
{outcomes.map((v) => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
<option key={v} value={v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Search
|
||||
<input value={needle} onChange={(e) => setNeedle(e.target.value)} placeholder="message, agent, actor" />
|
||||
<input
|
||||
value={needle}
|
||||
onChange={(e) => setNeedle(e.target.value)}
|
||||
placeholder="message, agent, actor"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p className="muted">Showing {filtered.length} of {events.length}</p>
|
||||
<p className="muted">
|
||||
Showing {filtered.length} of {events.length}
|
||||
</p>
|
||||
<pre className="output">{JSON.stringify(filtered, null, 2)}</pre>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,12 @@ type Props = {
|
||||
onAfterCommand?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function CommandsPage({ agents, selectedAgentId, onSelectAgent, onAfterCommand }: Props) {
|
||||
export function CommandsPage({
|
||||
agents,
|
||||
selectedAgentId,
|
||||
onSelectAgent,
|
||||
onAfterCommand,
|
||||
}: Props) {
|
||||
const [kind, setKind] = useState<CommandKind>("devs");
|
||||
const [query, setQuery] = useState("");
|
||||
const [output, setOutput] = useState("Select agent and run a command");
|
||||
@@ -37,8 +42,14 @@ export function CommandsPage({ agents, selectedAgentId, onSelectAgent, onAfterCo
|
||||
<form className="form" onSubmit={submit}>
|
||||
<label>
|
||||
Agent
|
||||
<select value={selectedAgentId} onChange={(e) => onSelectAgent(e.target.value)} required>
|
||||
<option value="" disabled>Select agent</option>
|
||||
<select
|
||||
value={selectedAgentId}
|
||||
onChange={(e) => onSelectAgent(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select agent
|
||||
</option>
|
||||
{agents.map((agent) => (
|
||||
<option key={agent.agent_id} value={agent.agent_id}>
|
||||
{agent.agent_id} ({agent.connected ? "connected" : "offline"})
|
||||
@@ -48,7 +59,10 @@ export function CommandsPage({ agents, selectedAgentId, onSelectAgent, onAfterCo
|
||||
</label>
|
||||
<label>
|
||||
Command
|
||||
<select value={kind} onChange={(e) => setKind(e.target.value as CommandKind)}>
|
||||
<select
|
||||
value={kind}
|
||||
onChange={(e) => setKind(e.target.value as CommandKind)}
|
||||
>
|
||||
<option value="devs">devs</option>
|
||||
<option value="leases">leases</option>
|
||||
<option value="inventory">inventory</option>
|
||||
@@ -57,9 +71,15 @@ export function CommandsPage({ agents, selectedAgentId, onSelectAgent, onAfterCo
|
||||
</label>
|
||||
<label>
|
||||
Query
|
||||
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="optional" />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="optional"
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" disabled={running || !selectedAgentId}>{running ? "Running..." : "Run command"}</button>
|
||||
<button type="submit" disabled={running || !selectedAgentId}>
|
||||
{running ? "Running..." : "Run command"}
|
||||
</button>
|
||||
</form>
|
||||
<pre className="output">{output}</pre>
|
||||
</section>
|
||||
|
||||
@@ -8,21 +8,42 @@ type Props = {
|
||||
onRefresh: () => void;
|
||||
};
|
||||
|
||||
export function DashboardPage({ agents, alerts, transitions, loading, onRefresh }: Props) {
|
||||
export function DashboardPage({
|
||||
agents,
|
||||
alerts,
|
||||
transitions,
|
||||
loading,
|
||||
onRefresh,
|
||||
}: Props) {
|
||||
const connected = agents.filter((a) => a.connected).length;
|
||||
return (
|
||||
<section className="card-grid">
|
||||
<div className="card stat"><h3>Agents</h3><strong>{agents.length}</strong></div>
|
||||
<div className="card stat"><h3>Connected</h3><strong>{connected}</strong></div>
|
||||
<div className="card stat"><h3>Active Alerts</h3><strong>{alerts.length}</strong></div>
|
||||
<div className="card stat"><h3>Transitions</h3><strong>{transitions.length}</strong></div>
|
||||
<div className="card stat">
|
||||
<h3>Agents</h3>
|
||||
<strong>{agents.length}</strong>
|
||||
</div>
|
||||
<div className="card stat">
|
||||
<h3>Connected</h3>
|
||||
<strong>{connected}</strong>
|
||||
</div>
|
||||
<div className="card stat">
|
||||
<h3>Active Alerts</h3>
|
||||
<strong>{alerts.length}</strong>
|
||||
</div>
|
||||
<div className="card stat">
|
||||
<h3>Transitions</h3>
|
||||
<strong>{transitions.length}</strong>
|
||||
</div>
|
||||
<div className="card span-full">
|
||||
<div className="row-head">
|
||||
<h2>Overview</h2>
|
||||
<button onClick={onRefresh} disabled={loading}>{loading ? "Refreshing..." : "Refresh"}</button>
|
||||
<button onClick={onRefresh} disabled={loading}>
|
||||
{loading ? "Refreshing..." : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
<p className="muted">
|
||||
Use tabs to run commands, inspect audits, and monitor alerts in real time.
|
||||
Use tabs to run commands, inspect audits, and monitor alerts in real
|
||||
time.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+218
-69
@@ -1,12 +1,14 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
// Utility for sorting
|
||||
const sorters = {
|
||||
name: (a: DeviceRow, b: DeviceRow) => a.name.localeCompare(b.name),
|
||||
ip: (a: DeviceRow, b: DeviceRow) => (a.ips[0] || "").localeCompare(b.ips[0] || ""),
|
||||
mac: (a: DeviceRow, b: DeviceRow) => (a.macs[0] || "").localeCompare(b.macs[0] || ""),
|
||||
presence: (a: DeviceRow, b: DeviceRow) => a.presence.localeCompare(b.presence),
|
||||
ip: (a: DeviceRow, b: DeviceRow) =>
|
||||
(a.ips[0] || "").localeCompare(b.ips[0] || ""),
|
||||
mac: (a: DeviceRow, b: DeviceRow) =>
|
||||
(a.macs[0] || "").localeCompare(b.macs[0] || ""),
|
||||
presence: (a: DeviceRow, b: DeviceRow) =>
|
||||
a.presence.localeCompare(b.presence),
|
||||
};
|
||||
|
||||
type SortKey = keyof typeof sorters;
|
||||
@@ -41,7 +43,12 @@ type WakeEvent = {
|
||||
};
|
||||
|
||||
const WAKE_HISTORY_KEY = "wakey_recent_wakes_v1";
|
||||
const PRESENCE_FILTERS: PresenceFilter[] = ["all", "online", "likely_online", "unknown"];
|
||||
const PRESENCE_FILTERS: PresenceFilter[] = [
|
||||
"all",
|
||||
"online",
|
||||
"likely_online",
|
||||
"unknown",
|
||||
];
|
||||
|
||||
function parseInventoryRows(payload: unknown): DeviceRow[] {
|
||||
if (!payload || typeof payload !== "object") return [];
|
||||
@@ -53,13 +60,20 @@ function parseInventoryRows(payload: unknown): DeviceRow[] {
|
||||
.map((raw, idx) => {
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const device = raw as Record<string, unknown>;
|
||||
const names = Array.isArray(device.names) ? device.names.filter((v) => typeof v === "string") as string[] : [];
|
||||
const ips = Array.isArray(device.ips) ? device.ips.filter((v) => typeof v === "string") as string[] : [];
|
||||
const macs = Array.isArray(device.macs) ? device.macs.filter((v) => typeof v === "string") as string[] : [];
|
||||
const interfaces = Array.isArray(device.interfaces)
|
||||
? device.interfaces.filter((v) => typeof v === "string") as string[]
|
||||
const names = Array.isArray(device.names)
|
||||
? (device.names.filter((v) => typeof v === "string") as string[])
|
||||
: [];
|
||||
const presence = typeof device.presence === "string" ? device.presence : "unknown";
|
||||
const ips = Array.isArray(device.ips)
|
||||
? (device.ips.filter((v) => typeof v === "string") as string[])
|
||||
: [];
|
||||
const macs = Array.isArray(device.macs)
|
||||
? (device.macs.filter((v) => typeof v === "string") as string[])
|
||||
: [];
|
||||
const interfaces = Array.isArray(device.interfaces)
|
||||
? (device.interfaces.filter((v) => typeof v === "string") as string[])
|
||||
: [];
|
||||
const presence =
|
||||
typeof device.presence === "string" ? device.presence : "unknown";
|
||||
|
||||
const id = macs[0] || ips[0] || names[0] || `row-${idx}`;
|
||||
return {
|
||||
@@ -74,17 +88,24 @@ function parseInventoryRows(payload: unknown): DeviceRow[] {
|
||||
.filter((v): v is DeviceRow => Boolean(v));
|
||||
}
|
||||
|
||||
function parseWakeSummary(response: unknown): { outcome: string; requestId: string; detail: string } {
|
||||
function parseWakeSummary(response: unknown): {
|
||||
outcome: string;
|
||||
requestId: string;
|
||||
detail: string;
|
||||
} {
|
||||
if (!response || typeof response !== "object") {
|
||||
return { outcome: "error", requestId: "", detail: "invalid wake response" };
|
||||
}
|
||||
const envelope = response as Record<string, unknown>;
|
||||
const requestId = typeof envelope.request_id === "string" ? envelope.request_id : "";
|
||||
const status = typeof envelope.status === "string" ? envelope.status : "error";
|
||||
const requestId =
|
||||
typeof envelope.request_id === "string" ? envelope.request_id : "";
|
||||
const status =
|
||||
typeof envelope.status === "string" ? envelope.status : "error";
|
||||
|
||||
if (status !== "ok") {
|
||||
const error = envelope.error as Record<string, unknown> | undefined;
|
||||
const detail = typeof error?.message === "string" ? error.message : "wake failed";
|
||||
const detail =
|
||||
typeof error?.message === "string" ? error.message : "wake failed";
|
||||
return { outcome: "error", requestId, detail };
|
||||
}
|
||||
|
||||
@@ -102,24 +123,32 @@ function parseWakeSummary(response: unknown): { outcome: string; requestId: stri
|
||||
const status =
|
||||
typeof statusRaw === "string"
|
||||
? statusRaw
|
||||
: (typeof (statusRaw as Record<string, unknown> | undefined)?.kind === "string"
|
||||
? String((statusRaw as Record<string, unknown>).kind)
|
||||
: "unknown");
|
||||
: typeof (statusRaw as Record<string, unknown> | undefined)?.kind ===
|
||||
"string"
|
||||
? String((statusRaw as Record<string, unknown>).kind)
|
||||
: "unknown";
|
||||
const ip = typeof rowObj.ip === "string" ? rowObj.ip : "?";
|
||||
const mac = typeof rowObj.mac === "string" ? rowObj.mac : "?";
|
||||
return `${status}(${ip}/${mac})`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const detail = targetOutcomes.length ? targetOutcomes.join(", ") : "wake dispatched";
|
||||
const failed = targetOutcomes.some((s) => s.startsWith("incomplete") || s.startsWith("nonexistent_address") || s.startsWith("wrong_size"));
|
||||
const detail = targetOutcomes.length
|
||||
? targetOutcomes.join(", ")
|
||||
: "wake dispatched";
|
||||
const failed = targetOutcomes.some(
|
||||
(s) =>
|
||||
s.startsWith("incomplete") ||
|
||||
s.startsWith("nonexistent_address") ||
|
||||
s.startsWith("wrong_size"),
|
||||
);
|
||||
return { outcome: failed ? "error" : "ok", requestId, detail };
|
||||
}
|
||||
|
||||
function chooseWakeTarget(device: DeviceRow): string {
|
||||
return device.name !== "(unnamed)"
|
||||
? device.name
|
||||
: (device.macs[0] || device.ips[0] || "");
|
||||
: device.macs[0] || device.ips[0] || "";
|
||||
}
|
||||
|
||||
function summarize(values: string[]): string {
|
||||
@@ -138,12 +167,12 @@ function loadHistory(): WakeEvent[] {
|
||||
if (!row || typeof row !== "object") return false;
|
||||
const event = row as Record<string, unknown>;
|
||||
return (
|
||||
typeof event.ts === "number"
|
||||
&& typeof event.target === "string"
|
||||
&& typeof event.agentId === "string"
|
||||
&& typeof event.outcome === "string"
|
||||
&& typeof event.requestId === "string"
|
||||
&& typeof event.detail === "string"
|
||||
typeof event.ts === "number" &&
|
||||
typeof event.target === "string" &&
|
||||
typeof event.agentId === "string" &&
|
||||
typeof event.outcome === "string" &&
|
||||
typeof event.requestId === "string" &&
|
||||
typeof event.detail === "string"
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
@@ -152,10 +181,18 @@ function loadHistory(): WakeEvent[] {
|
||||
}
|
||||
|
||||
function saveHistory(history: WakeEvent[]) {
|
||||
window.localStorage.setItem(WAKE_HISTORY_KEY, JSON.stringify(history.slice(0, 20)));
|
||||
window.localStorage.setItem(
|
||||
WAKE_HISTORY_KEY,
|
||||
JSON.stringify(history.slice(0, 20)),
|
||||
);
|
||||
}
|
||||
|
||||
export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWake }: Props) {
|
||||
export function DevicesPage({
|
||||
agents,
|
||||
selectedAgentId,
|
||||
onSelectAgent,
|
||||
onAfterWake,
|
||||
}: Props) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -173,11 +210,16 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
function getPresenceFromUrl(): PresenceFilter {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const raw = params.get("presence");
|
||||
return PRESENCE_FILTERS.includes(raw as PresenceFilter) ? (raw as PresenceFilter) : "all";
|
||||
return PRESENCE_FILTERS.includes(raw as PresenceFilter)
|
||||
? (raw as PresenceFilter)
|
||||
: "all";
|
||||
}
|
||||
|
||||
const [sort, setSort] = useState<{ key: SortKey; dir: SortDir }>(getSortFromUrl());
|
||||
const [presenceFilter, setPresenceFilter] = useState<PresenceFilter>(getPresenceFromUrl());
|
||||
const [sort, setSort] = useState<{ key: SortKey; dir: SortDir }>(
|
||||
getSortFromUrl(),
|
||||
);
|
||||
const [presenceFilter, setPresenceFilter] =
|
||||
useState<PresenceFilter>(getPresenceFromUrl());
|
||||
|
||||
// Keep view state in sync with URL
|
||||
useEffect(() => {
|
||||
@@ -221,7 +263,6 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setRecentWakes(loadHistory());
|
||||
}, []);
|
||||
@@ -257,7 +298,14 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
await wakeTarget(target, device.id);
|
||||
}
|
||||
|
||||
async function wakeTarget(target: string, busyId: string, opts: { refresh: boolean; notify: boolean } = { refresh: true, notify: true }) {
|
||||
async function wakeTarget(
|
||||
target: string,
|
||||
busyId: string,
|
||||
opts: { refresh: boolean; notify: boolean } = {
|
||||
refresh: true,
|
||||
notify: true,
|
||||
},
|
||||
) {
|
||||
if (!target || !selectedAgentId) return;
|
||||
setWakeBusyId(busyId);
|
||||
try {
|
||||
@@ -297,7 +345,10 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
for (const row of selectedRows) {
|
||||
const target = chooseWakeTarget(row);
|
||||
if (!target) continue;
|
||||
await wakeTarget(target, `bulk:${row.id}`, { refresh: false, notify: false });
|
||||
await wakeTarget(target, `bulk:${row.id}`, {
|
||||
refresh: false,
|
||||
notify: false,
|
||||
});
|
||||
}
|
||||
await loadInventory();
|
||||
if (onAfterWake) await onAfterWake();
|
||||
@@ -323,7 +374,9 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
}, [selectedAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIds((prev) => prev.filter((id) => rows.some((row) => row.id === id)));
|
||||
setSelectedIds((prev) =>
|
||||
prev.filter((id) => rows.some((row) => row.id === id)),
|
||||
);
|
||||
}, [rows]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -339,12 +392,13 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
result = result.filter((row) => row.presence === presenceFilter);
|
||||
}
|
||||
if (q) {
|
||||
result = result.filter((row) =>
|
||||
row.name.toLowerCase().includes(q)
|
||||
|| row.presence.toLowerCase().includes(q)
|
||||
|| row.ips.some((v) => v.toLowerCase().includes(q))
|
||||
|| row.macs.some((v) => v.toLowerCase().includes(q))
|
||||
|| row.interfaces.some((v) => v.toLowerCase().includes(q)),
|
||||
result = result.filter(
|
||||
(row) =>
|
||||
row.name.toLowerCase().includes(q) ||
|
||||
row.presence.toLowerCase().includes(q) ||
|
||||
row.ips.some((v) => v.toLowerCase().includes(q)) ||
|
||||
row.macs.some((v) => v.toLowerCase().includes(q)) ||
|
||||
row.interfaces.some((v) => v.toLowerCase().includes(q)),
|
||||
);
|
||||
}
|
||||
// Sort
|
||||
@@ -354,8 +408,11 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
return result;
|
||||
}, [rows, query, sort, presenceFilter]);
|
||||
|
||||
const selectedVisibleCount = filtered.filter((row) => selectedIds.includes(row.id)).length;
|
||||
const allVisibleSelected = filtered.length > 0 && selectedVisibleCount === filtered.length;
|
||||
const selectedVisibleCount = filtered.filter((row) =>
|
||||
selectedIds.includes(row.id),
|
||||
).length;
|
||||
const allVisibleSelected =
|
||||
filtered.length > 0 && selectedVisibleCount === filtered.length;
|
||||
|
||||
function toggleRowSelection(id: string, checked: boolean) {
|
||||
setSelectedIds((prev) => {
|
||||
@@ -366,7 +423,9 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
|
||||
function toggleAllVisible(checked: boolean) {
|
||||
if (!checked) {
|
||||
setSelectedIds((prev) => prev.filter((id) => !filtered.some((row) => row.id === id)));
|
||||
setSelectedIds((prev) =>
|
||||
prev.filter((id) => !filtered.some((row) => row.id === id)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setSelectedIds((prev) => {
|
||||
@@ -381,7 +440,10 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
<div className="card">
|
||||
<div className="row-head">
|
||||
<h2>Devices</h2>
|
||||
<button onClick={() => void loadInventory()} disabled={loading || !selectedAgentId}>
|
||||
<button
|
||||
onClick={() => void loadInventory()}
|
||||
disabled={loading || !selectedAgentId}
|
||||
>
|
||||
{loading ? "Refreshing..." : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -394,7 +456,9 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
onChange={(e) => onSelectAgent(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="" disabled>Select agent</option>
|
||||
<option value="" disabled>
|
||||
Select agent
|
||||
</option>
|
||||
{agents.map((agent) => (
|
||||
<option key={agent.agent_id} value={agent.agent_id}>
|
||||
{agent.agent_id} ({agent.connected ? "connected" : "offline"})
|
||||
@@ -423,13 +487,19 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
</label>
|
||||
<button
|
||||
onClick={() => void wakeTarget(quickWake.trim(), "quick")}
|
||||
disabled={!selectedAgentId || !quickWake.trim() || wakeBusyId === "quick"}
|
||||
disabled={
|
||||
!selectedAgentId || !quickWake.trim() || wakeBusyId === "quick"
|
||||
}
|
||||
>
|
||||
{wakeBusyId === "quick" ? "Waking..." : "Wake target"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="presence-filters" role="group" aria-label="Filter by presence">
|
||||
<div
|
||||
className="presence-filters"
|
||||
role="group"
|
||||
aria-label="Filter by presence"
|
||||
>
|
||||
{PRESENCE_FILTERS.map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
@@ -457,7 +527,9 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
disabled={!selectedVisibleCount || !selectedAgentId || bulkWakeBusy}
|
||||
type="button"
|
||||
>
|
||||
{bulkWakeBusy ? "Waking selected..." : `Wake selected (${selectedVisibleCount})`}
|
||||
{bulkWakeBusy
|
||||
? "Waking selected..."
|
||||
: `Wake selected (${selectedVisibleCount})`}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedIds([])}
|
||||
@@ -469,7 +541,9 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
{copyStatus && <span className="muted">{copyStatus}</span>}
|
||||
</div>
|
||||
|
||||
<p className="muted">Showing {filtered.length} of {rows.length}</p>
|
||||
<p className="muted">
|
||||
Showing {filtered.length} of {rows.length}
|
||||
</p>
|
||||
{error && <pre className="error">{error}</pre>}
|
||||
<div className="list device-list">
|
||||
<div className="row plain device-row device-header">
|
||||
@@ -484,27 +558,48 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
</span>
|
||||
<span
|
||||
className="sortable-col device-cell"
|
||||
onClick={() => setSort((s) => ({ key: "name", dir: s.key === "name" && s.dir === "asc" ? "desc" : "asc" }))}
|
||||
onClick={() =>
|
||||
setSort((s) => ({
|
||||
key: "name",
|
||||
dir: s.key === "name" && s.dir === "asc" ? "desc" : "asc",
|
||||
}))
|
||||
}
|
||||
>
|
||||
Name {sort.key === "name" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
||||
</span>
|
||||
<span
|
||||
className="sortable-col device-cell"
|
||||
onClick={() => setSort((s) => ({ key: "ip", dir: s.key === "ip" && s.dir === "asc" ? "desc" : "asc" }))}
|
||||
onClick={() =>
|
||||
setSort((s) => ({
|
||||
key: "ip",
|
||||
dir: s.key === "ip" && s.dir === "asc" ? "desc" : "asc",
|
||||
}))
|
||||
}
|
||||
>
|
||||
IP {sort.key === "ip" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
||||
</span>
|
||||
<span
|
||||
className="sortable-col device-cell"
|
||||
onClick={() => setSort((s) => ({ key: "mac", dir: s.key === "mac" && s.dir === "asc" ? "desc" : "asc" }))}
|
||||
onClick={() =>
|
||||
setSort((s) => ({
|
||||
key: "mac",
|
||||
dir: s.key === "mac" && s.dir === "asc" ? "desc" : "asc",
|
||||
}))
|
||||
}
|
||||
>
|
||||
MAC {sort.key === "mac" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
||||
</span>
|
||||
<span
|
||||
className="sortable-col device-cell"
|
||||
onClick={() => setSort((s) => ({ key: "presence", dir: s.key === "presence" && s.dir === "asc" ? "desc" : "asc" }))}
|
||||
onClick={() =>
|
||||
setSort((s) => ({
|
||||
key: "presence",
|
||||
dir: s.key === "presence" && s.dir === "asc" ? "desc" : "asc",
|
||||
}))
|
||||
}
|
||||
>
|
||||
Presence {sort.key === "presence" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
||||
Presence{" "}
|
||||
{sort.key === "presence" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
||||
</span>
|
||||
<span className="device-cell">Interfaces</span>
|
||||
<span className="device-cell device-action">Actions</span>
|
||||
@@ -519,18 +614,63 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
aria-label={`Select ${row.name}`}
|
||||
/>
|
||||
</span>
|
||||
<span className="device-cell" data-label="Name" title={row.name}>{row.name}</span>
|
||||
<span className="device-cell muted" data-label="IP" title={row.ips.join(", ") || "-"}>{summarize(row.ips)}</span>
|
||||
<span className="device-cell muted" data-label="MAC" title={row.macs.join(", ") || "-"}>{summarize(row.macs)}</span>
|
||||
<span className="device-cell" data-label="Presence"><span className="pill">{row.presence}</span></span>
|
||||
<span className="device-cell" data-label="Interfaces" title={row.interfaces.join(", ") || "-"}>{summarize(row.interfaces)}</span>
|
||||
<span className="device-cell" data-label="Name" title={row.name}>
|
||||
{row.name}
|
||||
</span>
|
||||
<span
|
||||
className="device-cell muted"
|
||||
data-label="IP"
|
||||
title={row.ips.join(", ") || "-"}
|
||||
>
|
||||
{summarize(row.ips)}
|
||||
</span>
|
||||
<span
|
||||
className="device-cell muted"
|
||||
data-label="MAC"
|
||||
title={row.macs.join(", ") || "-"}
|
||||
>
|
||||
{summarize(row.macs)}
|
||||
</span>
|
||||
<span className="device-cell" data-label="Presence">
|
||||
<span className="pill">{row.presence}</span>
|
||||
</span>
|
||||
<span
|
||||
className="device-cell"
|
||||
data-label="Interfaces"
|
||||
title={row.interfaces.join(", ") || "-"}
|
||||
>
|
||||
{summarize(row.interfaces)}
|
||||
</span>
|
||||
<span className="device-cell device-action" data-label="">
|
||||
<button onClick={() => void wakeDevice(row)} disabled={wakeBusyId === row.id || !selectedAgentId || bulkWakeBusy}>
|
||||
<button
|
||||
onClick={() => void wakeDevice(row)}
|
||||
disabled={
|
||||
wakeBusyId === row.id || !selectedAgentId || bulkWakeBusy
|
||||
}
|
||||
>
|
||||
{wakeBusyId === row.id ? "Waking..." : "Wake"}
|
||||
</button>
|
||||
<button type="button" className="mini-btn" onClick={() => void copyValue("name", chooseWakeTarget(row))}>Copy name</button>
|
||||
<button type="button" className="mini-btn" onClick={() => void copyValue("ip", row.ips[0] || "")}>Copy IP</button>
|
||||
<button type="button" className="mini-btn" onClick={() => void copyValue("mac", row.macs[0] || "")}>Copy MAC</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mini-btn"
|
||||
onClick={() => void copyValue("name", chooseWakeTarget(row))}
|
||||
>
|
||||
Copy name
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mini-btn"
|
||||
onClick={() => void copyValue("ip", row.ips[0] || "")}
|
||||
>
|
||||
Copy IP
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mini-btn"
|
||||
onClick={() => void copyValue("mac", row.macs[0] || "")}
|
||||
>
|
||||
Copy MAC
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -552,10 +692,15 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
</div>
|
||||
<div className="list">
|
||||
{recentWakes.map((event, idx) => (
|
||||
<div className="row plain" key={`${event.ts}-${event.target}-${idx}`}>
|
||||
<div
|
||||
className="row plain"
|
||||
key={`${event.ts}-${event.target}-${idx}`}
|
||||
>
|
||||
<div>
|
||||
<strong>{event.target}</strong>
|
||||
<div className="muted">{new Date(event.ts).toLocaleString()} on {event.agentId}</div>
|
||||
<div className="muted">
|
||||
{new Date(event.ts).toLocaleString()} on {event.agentId}
|
||||
</div>
|
||||
<div className="muted">{event.detail}</div>
|
||||
{event.requestId && (
|
||||
<Link
|
||||
@@ -566,12 +711,16 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<span className={`pill ${event.outcome === "ok" ? "ready" : "error"}`}>
|
||||
<span
|
||||
className={`pill ${event.outcome === "ok" ? "ready" : "error"}`}
|
||||
>
|
||||
{event.outcome}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{!recentWakes.length && <div className="empty">No wake actions yet</div>}
|
||||
{!recentWakes.length && (
|
||||
<div className="empty">No wake actions yet</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -19,7 +19,10 @@ export function TokensPage() {
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const activeCount = useMemo(() => tokens.filter((t) => !t.expired).length, [tokens]);
|
||||
const activeCount = useMemo(
|
||||
() => tokens.filter((t) => !t.expired).length,
|
||||
[tokens],
|
||||
);
|
||||
|
||||
async function load() {
|
||||
setBusy(true);
|
||||
@@ -41,7 +44,9 @@ export function TokensPage() {
|
||||
try {
|
||||
const ttl = Number.parseInt(ttlSeconds, 10);
|
||||
const issued = await issueEnrollToken(Number.isFinite(ttl) ? ttl : 86400);
|
||||
setStatus(`Issued ${issued.enroll_token} (expires ${formatUnix(issued.expires_at_unix)})`);
|
||||
setStatus(
|
||||
`Issued ${issued.enroll_token} (expires ${formatUnix(issued.expires_at_unix)})`,
|
||||
);
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(String(err));
|
||||
@@ -56,7 +61,9 @@ export function TokensPage() {
|
||||
setStatus("");
|
||||
try {
|
||||
const result = await revokeEnrollToken(token);
|
||||
setStatus(result.revoked ? `Revoked ${token}` : `${token} was already absent`);
|
||||
setStatus(
|
||||
result.revoked ? `Revoked ${token}` : `${token} was already absent`,
|
||||
);
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(String(err));
|
||||
@@ -74,7 +81,9 @@ export function TokensPage() {
|
||||
<div className="card">
|
||||
<div className="row-head">
|
||||
<h2>Enroll Tokens</h2>
|
||||
<button onClick={() => void load()} disabled={busy}>Refresh</button>
|
||||
<button onClick={() => void load()} disabled={busy}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div className="form">
|
||||
<label>
|
||||
@@ -88,19 +97,26 @@ export function TokensPage() {
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p className="muted">{activeCount} active, {tokens.length} shown</p>
|
||||
<p className="muted">
|
||||
{activeCount} active, {tokens.length} shown
|
||||
</p>
|
||||
<div className="list">
|
||||
{tokens.map((token) => (
|
||||
<div className="row" key={token.enroll_token}>
|
||||
<div>
|
||||
<strong>{token.enroll_token}</strong>
|
||||
<div className="muted">expires: {formatUnix(token.expires_at_unix)}</div>
|
||||
<div className="muted">
|
||||
expires: {formatUnix(token.expires_at_unix)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="token-actions">
|
||||
<span className={`pill ${token.expired ? "error" : "ready"}`}>
|
||||
{token.expired ? "expired" : "active"}
|
||||
</span>
|
||||
<button onClick={() => void onRevoke(token.enroll_token)} disabled={busy}>
|
||||
<button
|
||||
onClick={() => void onRevoke(token.enroll_token)}
|
||||
disabled={busy}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
@@ -122,7 +138,9 @@ export function TokensPage() {
|
||||
onChange={(e) => setTtlSeconds(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button onClick={() => void onIssue()} disabled={busy}>Issue</button>
|
||||
<button onClick={() => void onIssue()} disabled={busy}>
|
||||
Issue
|
||||
</button>
|
||||
</div>
|
||||
{status && <pre className="output">{status}</pre>}
|
||||
{error && <pre className="error">{error}</pre>}
|
||||
|
||||
+100
-26
@@ -6,7 +6,9 @@
|
||||
--muted: #9bb0d1;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
@@ -14,7 +16,9 @@ body {
|
||||
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.app { padding: 1rem; }
|
||||
.app {
|
||||
padding: 1rem;
|
||||
}
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -22,8 +26,13 @@ body {
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.topbar h1 { margin: 0; }
|
||||
.topbar p { margin: 0.35rem 0 0; color: var(--muted); }
|
||||
.topbar h1 {
|
||||
margin: 0;
|
||||
}
|
||||
.topbar p {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
@@ -52,9 +61,18 @@ body {
|
||||
padding: 0.35rem 0.7rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.pill.ready { color: #86efac; border-color: #14532d; }
|
||||
.pill.loading { color: #fde68a; border-color: #713f12; }
|
||||
.pill.error { color: #fca5a5; border-color: #7f1d1d; }
|
||||
.pill.ready {
|
||||
color: #86efac;
|
||||
border-color: #14532d;
|
||||
}
|
||||
.pill.loading {
|
||||
color: #fde68a;
|
||||
border-color: #713f12;
|
||||
}
|
||||
.pill.error {
|
||||
color: #fca5a5;
|
||||
border-color: #7f1d1d;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
@@ -68,8 +86,14 @@ body {
|
||||
gap: 0.8rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
.stat h3 { margin: 0; color: var(--muted); font-weight: 500; }
|
||||
.stat strong { font-size: 1.5rem; }
|
||||
.stat h3 {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.stat strong {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
@@ -85,26 +109,60 @@ body {
|
||||
.card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, rgba(17,26,45,0.92), rgba(17,26,45,0.72));
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(17, 26, 45, 0.92),
|
||||
rgba(17, 26, 45, 0.72)
|
||||
);
|
||||
padding: 0.8rem;
|
||||
}
|
||||
.span-2 { grid-column: 1 / -1; }
|
||||
.span-full { grid-column: 1 / -1; }
|
||||
.row-head { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; }
|
||||
.muted { color: var(--muted); }
|
||||
.span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.span-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.row-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
button, input, select {
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
background: #0b1528;
|
||||
color: var(--text);
|
||||
}
|
||||
button { padding: 0.45rem 0.65rem; cursor: pointer; }
|
||||
button:hover { border-color: #6b84ad; }
|
||||
input, select { width: 100%; padding: 0.45rem 0.55rem; }
|
||||
button {
|
||||
padding: 0.45rem 0.65rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #6b84ad;
|
||||
}
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.45rem 0.55rem;
|
||||
}
|
||||
|
||||
.form { display: grid; gap: 0.65rem; }
|
||||
.form label { display: grid; gap: 0.35rem; color: var(--muted); font-size: 0.9rem; }
|
||||
.form {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.form label {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
display: grid;
|
||||
@@ -182,7 +240,11 @@ input, select { width: 100%; padding: 0.45rem 0.55rem; }
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.list { display: grid; gap: 0.4rem; margin-top: 0.55rem; }
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.55rem;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -194,8 +256,12 @@ input, select { width: 100%; padding: 0.45rem 0.55rem; }
|
||||
padding: 0.45rem 0.55rem;
|
||||
text-align: left;
|
||||
}
|
||||
.row.selected { border-color: #6b84ad; }
|
||||
.row.plain { cursor: default; }
|
||||
.row.selected {
|
||||
border-color: #6b84ad;
|
||||
}
|
||||
.row.plain {
|
||||
cursor: default;
|
||||
}
|
||||
.token-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -207,7 +273,10 @@ input, select { width: 100%; padding: 0.45rem 0.55rem; }
|
||||
|
||||
.device-row {
|
||||
display: grid;
|
||||
grid-template-columns: 36px minmax(110px, 1.1fr) minmax(160px, 2fr) minmax(130px, 1.5fr) minmax(110px, 0.9fr) minmax(120px, 1fr) minmax(220px, auto);
|
||||
grid-template-columns: 36px minmax(110px, 1.1fr) minmax(160px, 2fr) minmax(
|
||||
130px,
|
||||
1.5fr
|
||||
) minmax(110px, 0.9fr) minmax(120px, 1fr) minmax(220px, auto);
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
@@ -318,7 +387,10 @@ input, select { width: 100%; padding: 0.45rem 0.55rem; }
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.empty { color: var(--muted); padding: 0.35rem 0.2rem; }
|
||||
.empty {
|
||||
color: var(--muted);
|
||||
padding: 0.35rem 0.2rem;
|
||||
}
|
||||
|
||||
.output {
|
||||
margin: 0.65rem 0 0;
|
||||
@@ -330,7 +402,9 @@ input, select { width: 100%; padding: 0.45rem 0.55rem; }
|
||||
overflow: auto;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
.output.small { max-height: 230px; }
|
||||
.output.small {
|
||||
max-height: 230px;
|
||||
}
|
||||
.error {
|
||||
border: 1px solid #7f1d1d;
|
||||
background: rgba(127, 29, 29, 0.2);
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@ function sourcemapEnabled(): boolean {
|
||||
}
|
||||
|
||||
const ReactCompilerConfig = {
|
||||
target: '18',
|
||||
runtimeModule: 'react-compiler-runtime', // Redirects the missing specifier
|
||||
target: "18",
|
||||
runtimeModule: "react-compiler-runtime", // Redirects the missing specifier
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
Reference in New Issue
Block a user