diff --git a/app/components/Select.tsx b/app/components/Select.tsx index 096edea..5df6e21 100644 --- a/app/components/Select.tsx +++ b/app/components/Select.tsx @@ -50,11 +50,11 @@ function Select(props: SelectProps) {
@@ -63,18 +63,18 @@ function Select(props: SelectProps) { 'flex rounded-xl focus:outline-hidden focus-within:ring-3', 'bg-white dark:bg-headplane-900', 'border border-headplane-100 dark:border-headplane-800', + props.isInvalid && 'ring-red-400', )} > @@ -99,12 +100,12 @@ function Select(props: SelectProps) { )} {state.isOpen && ( @@ -114,23 +115,22 @@ function Select(props: SelectProps) { } interface ListBoxProps extends AriaListBoxOptions { - listBoxRef?: React.RefObject; + listBoxRef: React.RefObject; state: ListState; } function ListBox(props: ListBoxProps) { const { listBoxRef, state } = props; - const ref = listBoxRef ?? useRef(null); - const { listBoxProps } = useListBox(props, state, ref); + const { listBoxProps } = useListBox(props, state, listBoxRef); return (
    {[...state.collection].map((item) => ( -
); @@ -154,7 +154,6 @@ function Option({ item, state }: OptionProps) { return (
  • {item.rendered} {isSelected && } diff --git a/app/routes/machines/components/machine-row.tsx b/app/routes/machines/components/machine-row.tsx index cb44738..aa4dea9 100644 --- a/app/routes/machines/components/machine-row.tsx +++ b/app/routes/machines/components/machine-row.tsx @@ -22,6 +22,7 @@ interface Props { isAgent?: boolean; magic?: string; isDisabled?: boolean; + existingTags?: string[]; } export default function MachineRow({ @@ -30,6 +31,7 @@ export default function MachineRow({ isAgent, magic, isDisabled, + existingTags, }: Props) { const uiTags = useMemo(() => { const tags = uiTagsForNode(node, isAgent); @@ -146,6 +148,7 @@ export default function MachineRow({ (null); const supportsTailscaleSSH = @@ -75,6 +77,7 @@ export default function MachineMenu({ )} {modal === 'tags' && ( { diff --git a/app/routes/machines/dialogs/tags.tsx b/app/routes/machines/dialogs/tags.tsx index b00d081..bbc280f 100644 --- a/app/routes/machines/dialogs/tags.tsx +++ b/app/routes/machines/dialogs/tags.tsx @@ -1,9 +1,9 @@ import { Plus, TagsIcon, X } from 'lucide-react'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import Button from '~/components/Button'; import Dialog from '~/components/Dialog'; -import Input from '~/components/Input'; import Link from '~/components/Link'; +import Select from '~/components/Select'; import TableList from '~/components/TableList'; import type { Machine } from '~/types'; import cn from '~/utils/cn'; @@ -12,11 +12,24 @@ interface TagsProps { machine: Machine; isOpen: boolean; setIsOpen: (isOpen: boolean) => void; + existingTags?: string[]; } -export default function Tags({ machine, isOpen, setIsOpen }: TagsProps) { +export default function Tags({ + machine, + isOpen, + setIsOpen, + existingTags, +}: TagsProps) { const [tags, setTags] = useState(machine.forcedTags); - const [tag, setTag] = useState(''); + const [tag, setTag] = useState('tag:'); + const tagIsInvalid = useMemo(() => { + return tag.length === 0 || !tag.startsWith('tag:') || tags.includes(tag); + }, [tag, tags]); + + const validNodeTags = useMemo(() => { + return existingTags?.filter((nodeTag) => !tags.includes(nodeTag)) || []; + }, [tags]); return ( @@ -57,44 +70,36 @@ export default function Tags({ machine, isOpen, setIsOpen }: TagsProps) { )) )} - 0 && - (!tag.startsWith('tag:') || tags.includes(tag)) && - 'ring-3 ring-red-500 ring-opacity-50', - )} - > - - - + +
    + + +
    ); diff --git a/app/routes/machines/machine.tsx b/app/routes/machines/machine.tsx index c054ed5..0b751f0 100644 --- a/app/routes/machines/machine.tsx +++ b/app/routes/machines/machine.tsx @@ -10,7 +10,7 @@ import StatusCircle from '~/components/StatusCircle'; import Tooltip from '~/components/Tooltip'; import cn from '~/utils/cn'; import { getOSInfo, getTSVersion } from '~/utils/host-info'; -import { mapNodes } from '~/utils/node-info'; +import { mapNodes, sortNodeTags } from '~/utils/node-info'; import type { Route } from './+types/machine'; import { mapTagsToComponents, uiTagsForNode } from './components/machine-row'; import MenuOptions from './components/menu'; @@ -35,10 +35,8 @@ export async function loader({ request, params, context }: Route.LoaderArgs) { } const api = context.hsApi.getRuntimeClient(session.api_key); - const [node, users] = await Promise.all([ - api.getNode(params.id), - api.getUsers(), - ]); + const [nodes, users] = await Promise.all([api.getNodes(), api.getUsers()]); + const node = nodes.find((node) => node.id === params.id); const lookup = await context.agents?.lookup([node.nodeKey]); const [enhancedNode] = mapNodes([node], lookup); @@ -53,13 +51,14 @@ export async function loader({ request, params, context }: Route.LoaderArgs) { magic, agent: context.agents?.agentID(), stats: lookup?.[enhancedNode.nodeKey], + existingTags: sortNodeTags(nodes), }; } export const action = machineAction; export default function Page({ - loaderData: { node, tags, users, magic, agent, stats }, + loaderData: { node, tags, users, magic, agent, stats, existingTags }, }: Route.ComponentProps) { const [showRouting, setShowRouting] = useState(false); @@ -87,7 +86,13 @@ export default function Page({

    {node.givenName}

    - +
    diff --git a/app/routes/machines/overview.tsx b/app/routes/machines/overview.tsx index cf99c1c..3b23f8a 100644 --- a/app/routes/machines/overview.tsx +++ b/app/routes/machines/overview.tsx @@ -4,7 +4,7 @@ import Link from '~/components/Link'; import Tooltip from '~/components/Tooltip'; import { Capabilities } from '~/server/web/roles'; import cn from '~/utils/cn'; -import { mapNodes } from '~/utils/node-info'; +import { mapNodes, sortNodeTags } from '~/utils/node-info'; import type { Route } from './+types/overview'; import MachineRow from './components/machine-row'; import NewMachine from './dialogs/new'; @@ -126,6 +126,7 @@ export default function Page({ loaderData }: Route.ComponentProps) { > {loaderData.populatedNodes.map((node) => ( [ + ...validTags, + ...forcedTags, + ]), + ), + ).sort(); +}