Add key expiry disable/enable toggle (#554)

This commit is contained in:
Julian Lehrhuber
2026-08-19 03:41:30 +02:00
committed by GitHub
parent 0feab692bd
commit cd59c310f6
9 changed files with 89 additions and 5 deletions
@@ -28,6 +28,7 @@ interface Props {
isDisabled?: boolean;
existingTags?: string[];
supportsNodeOwnerChange: boolean;
supportsDisablingKeyExpiry: boolean;
}
export default function MachineRow({
@@ -38,6 +39,7 @@ export default function MachineRow({
isDisabled,
existingTags,
supportsNodeOwnerChange,
supportsDisablingKeyExpiry,
}: Props) {
const uiTags = useMemo(() => uiTagsForNode(node, isAgent), [node, isAgent]);
@@ -144,6 +146,7 @@ export default function MachineRow({
node={node}
users={users}
supportsNodeOwnerChange={supportsNodeOwnerChange}
supportsDisablingKeyExpiry={supportsDisablingKeyExpiry}
/>
</td>
</tr>
+26 -4
View File
@@ -1,11 +1,12 @@
import { Cog, Ellipsis, SquareTerminal } from "lucide-react";
import { useState } from "react";
import { useSubmit } from "react-router";
import Button from "~/components/button";
import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from "~/components/menu";
import type { User } from "~/types";
import cn from "~/utils/cn";
import { PopulatedNode } from "~/utils/node-info";
import { isNoExpiry, type PopulatedNode } from "~/utils/node-info";
import Delete from "../dialogs/delete";
import Expire from "../dialogs/expire";
@@ -22,6 +23,7 @@ interface MenuProps {
isDisabled?: boolean;
existingTags?: string[];
supportsNodeOwnerChange: boolean;
supportsDisablingKeyExpiry: boolean;
}
type Modal = "rename" | "expire" | "remove" | "routes" | "move" | "tags" | null;
@@ -34,7 +36,9 @@ export default function MachineMenu({
isDisabled,
existingTags,
supportsNodeOwnerChange,
supportsDisablingKeyExpiry,
}: MenuProps) {
const submit = useSubmit();
const [modal, setModal] = useState<Modal>(null);
const supportsTailscaleSSH = node.hostInfo?.sshHostKeys && node.hostInfo?.sshHostKeys.length > 0;
@@ -156,15 +160,33 @@ export default function MachineMenu({
</MenuTrigger>
<MenuContent>
<MenuItem onClick={() => setModal("rename")}>Edit machine name</MenuItem>
{supportsDisablingKeyExpiry && (
<MenuItem
onClick={() =>
submit(
{
action_id: "toggle_expiry",
node_id: node.id,
disableExpiry: !isNoExpiry(node.expiry),
},
{ method: "post" },
)
}
>
{isNoExpiry(node.expiry) ? "Enable" : "Disable"} key expiry
</MenuItem>
)}
<MenuItem onClick={() => setModal("routes")}>Edit route settings</MenuItem>
<MenuItem onClick={() => setModal("tags")}>Edit ACL tags</MenuItem>
{supportsNodeOwnerChange && (
<MenuItem onClick={() => setModal("move")}>Change owner</MenuItem>
)}
<MenuSeparator />
<MenuItem variant="danger" disabled={node.expired} onClick={() => setModal("expire")}>
Expire
</MenuItem>
{!isNoExpiry(node.expiry) && (
<MenuItem variant="danger" disabled={node.expired} onClick={() => setModal("expire")}>
Expire
</MenuItem>
)}
<MenuItem variant="danger" onClick={() => setModal("remove")}>
Remove
</MenuItem>
+7
View File
@@ -113,6 +113,13 @@ export async function machineAction({ request, context }: Route.ActionArgs) {
return { message: "Machine expired" };
}
case "toggle_expiry": {
const disableExpiry = String(formData.get("disableExpiry")) === "true";
await api.nodes.toggleExpiry(nodeId, disableExpiry);
await headscaleLiveStore.refresh(nodesResource, api);
return { message: "Machine expired" };
}
case "update_tags": {
const tags = formData.get("tags")?.toString().split(",") ?? [];
if (tags.length === 0) {
+14 -1
View File
@@ -66,6 +66,7 @@ export async function loader({ request, params, context }: Route.LoaderArgs) {
const [enhancedNode] = mapNodes([node], stats);
const tags = [...node.tags].toSorted();
const supportsNodeOwnerChange = !headscale.capabilities.nodeOwnerIsImmutable;
const supportsDisablingKeyExpiry = headscale.capabilities.keyExpiryCanBeDisabled;
const agentSync = agents?.lastSync();
const policy = policyResult.status === "fulfilled" ? policyResult.value.policy : undefined;
@@ -82,6 +83,7 @@ export async function loader({ request, params, context }: Route.LoaderArgs) {
node: enhancedNode,
stats: stats?.[enhancedNode.nodeKey],
supportsNodeOwnerChange: supportsNodeOwnerChange,
supportsDisablingKeyExpiry: supportsDisablingKeyExpiry,
tags,
users,
};
@@ -90,7 +92,17 @@ export async function loader({ request, params, context }: Route.LoaderArgs) {
export const action = machineAction;
export default function Page({
loaderData: { node, tags, users, magic, agent, stats, existingTags, supportsNodeOwnerChange },
loaderData: {
node,
tags,
users,
magic,
agent,
stats,
existingTags,
supportsNodeOwnerChange,
supportsDisablingKeyExpiry,
},
}: Route.ComponentProps) {
const [showRouting, setShowRouting] = useState(false);
@@ -125,6 +137,7 @@ export default function Page({
node={node}
users={users}
supportsNodeOwnerChange={supportsNodeOwnerChange}
supportsDisablingKeyExpiry={supportsDisablingKeyExpiry}
/>
</div>
<div className="mb-4 flex gap-1">
+3
View File
@@ -67,6 +67,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
const policy = policyResult.status === "fulfilled" ? policyResult.value.policy : undefined;
const populatedNodes = mapNodes(nodes, stats);
const supportsNodeOwnerChange = !headscale.capabilities.nodeOwnerIsImmutable;
const supportsDisablingKeyExpiry = headscale.capabilities.keyExpiryCanBeDisabled;
const agentSync = agents?.lastSync();
return {
@@ -86,6 +87,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
publicServer: config.headscale.public_url,
server: config.headscale.url,
supportsNodeOwnerChange: supportsNodeOwnerChange,
supportsDisablingKeyExpiry: supportsDisablingKeyExpiry,
users,
writable: writablePermission,
};
@@ -458,6 +460,7 @@ export default function Page({ loaderData }: Route.ComponentProps) {
node={node}
users={loaderData.users}
supportsNodeOwnerChange={loaderData.supportsNodeOwnerChange}
supportsDisablingKeyExpiry={loaderData.supportsDisablingKeyExpiry}
/>
))
)}