import { useReducer, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import * as stylex from "@stylexjs/stylex"; import { clientPrefixesPutMutation } from "@/lib/queries"; import type { ClientPrefix, Group } from "@/lib/types"; import { defaultGroupId } from "@/lib/defaultGroup"; import { firstProblem, initPrefixEditor, isDirty, prefixEditorReducer, toInputs } from "./prefixEditor"; import InlineError from "@/lib/InlineError"; import AuthorityGate from "@/features/configuration/AuthorityGate"; import Select from "@/ui/Select"; import { styles as shared } from "@/ui/styles"; import { colors } from "@/ui/tokens.stylex"; interface Props { prefixes: ClientPrefix[]; groups: Group[]; } const styles = stylex.create({ section: { marginTop: "2.5rem", }, heading: { fontSize: "1.25rem", lineHeight: "1.75rem", fontWeight: 600, }, headingKey: { marginLeft: "0.5rem", fontSize: "0.75rem", lineHeight: "1rem", fontWeight: 400, color: colors.textMuted, }, intro: { marginTop: "0.25rem", fontSize: "0.875rem", lineHeight: "1.25rem", color: colors.textMuted, }, empty: { marginTop: "1rem", fontSize: "0.875rem", lineHeight: "1.25rem", color: colors.textMuted, }, rows: { display: "flex", flexDirection: "column", gap: "0.5rem", marginTop: "1rem", listStyleType: "none", padding: 0, }, row: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: "0.5rem", }, prefixInput: { width: "13rem", }, priorityInput: { width: "5rem", }, removeButton: { borderRadius: "0.25rem", borderWidth: 1, borderStyle: "solid", borderColor: colors.borderStrong, backgroundColor: "transparent", paddingInline: "0.5rem", paddingBlock: "0.375rem", fontSize: "0.875rem", lineHeight: "1.25rem", color: colors.danger, }, validation: { marginTop: "0.5rem", fontSize: "0.875rem", lineHeight: "1.25rem", color: colors.danger, }, actions: { display: "flex", gap: "0.5rem", marginTop: "1rem", }, table: { width: "100%", minWidth: "max-content", borderCollapse: "collapse", fontSize: "0.875rem", lineHeight: "1.25rem", }, }); /** * Address ranges that assign a group to every device inside them. * * The section is a configuration rendering, so it goes through the authority * gate: the editor exists only where a save can land, and file authority gets * the assignments as a table rather than a form nobody may submit. */ export default function NetworkAssignments({ prefixes, groups }: Props) { return (

Network assignments client_prefixes

An address range assigns its group to every device inside it, for devices with no row of their own. The highest priority match wins.

{(status) => status.authority === "database" ? ( ) : ( ) }
); } function AssignmentsTable({ prefixes }: { prefixes: ClientPrefix[] }) { if (prefixes.length === 0) return

The file declares no network assignments.

; return (
{prefixes.map((prefix) => ( ))}
Range Group Priority
{prefix.prefix} {prefix.group} {prefix.priority}
); } /** * The list is saved as a whole, so the editor holds every row and the PUT * replaces the set. Reached only under resolved database authority: the gate * above owns that decision, and no control here consults it a second time. */ function AssignmentsEditor({ prefixes, groups }: Props) { const queryClient = useQueryClient(); const mutation = useMutation(clientPrefixesPutMutation(queryClient)); const [state, dispatch] = useReducer(prefixEditorReducer, prefixes, initPrefixEditor); const [validation, setValidation] = useState(null); const dirty = isDirty(state); const fallbackGroupId = defaultGroupId(groups); const groupOptions = groups.map((group) => ({ value: String(group.id), label: group.name })); const save = () => { const problem = firstProblem(state.rows); setValidation(problem); if (problem !== null) return; mutation.mutate(toInputs(state.rows), { onSuccess: (stored) => dispatch({ type: "reset", prefixes: stored }), }); }; return ( <> {state.rows.length === 0 ? (

No network assignments configured.

) : ( )} {validation !== null && (

{validation}

)}
{dirty && ( )}
); }