ostool-server 0.7.2

Server for managing development boards, serial sessions, and TFTP artifacts
import {
  useId,
  useRef,
  useState,
  type ReactNode,
  type ComponentProps,
} from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { NativeSelect } from "@/components/ui/native-select";
import { Checkbox } from "@/components/ui/checkbox";
import {
  Field,
  FieldGroup,
  FieldLabel,
  FieldDescription,
} from "@/components/ui/field";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
  Dialog,
  DialogContent,
  DialogTitle,
  DialogDescription,
  DialogFooter,
} from "@/components/ui/dialog";
import {
  Table,
  TableHeader,
  TableRow,
  TableHead,
  TableBody,
  TableCell,
} from "@/components/ui/table";
import {
  Empty,
  EmptyHeader,
  EmptyTitle,
  EmptyDescription,
} from "@/components/ui/empty";

export function TextField({
  label,
  hint,
  onValue,
  ...props
}: ComponentProps<typeof Input> & {
  label: string;
  hint?: string;
  onValue?: (value: string) => void;
}) {
  const id = useId();
  return (
    <Field>
      <FieldLabel htmlFor={id}>{label}</FieldLabel>
      <Input
        id={id}
        {...props}
        onChange={onValue ? (e) => onValue(e.target.value) : props.onChange}
      />
      {hint && <FieldDescription>{hint}</FieldDescription>}
    </Field>
  );
}
export function TextareaField({
  label,
  value,
  onValue,
  hint,
}: {
  label: string;
  value: string;
  onValue: (value: string) => void;
  hint?: string;
}) {
  const id = useId();
  return (
    <Field>
      <FieldLabel htmlFor={id}>{label}</FieldLabel>
      <Textarea
        id={id}
        value={value}
        onChange={(e) => onValue(e.target.value)}
        rows={2}
      />
      {hint && <FieldDescription>{hint}</FieldDescription>}
    </Field>
  );
}
export function SelectField({
  label,
  options,
  onValue,
  ...props
}: Omit<ComponentProps<typeof NativeSelect>, "onChange"> & {
  label: string;
  options: { value: string; label: string; disabled?: boolean }[];
  onValue: (value: string) => void;
}) {
  const id = useId();
  const optionsWithCurrent = [...options];
  if (props.value && !options.some((o) => o.value === props.value))
    optionsWithCurrent.unshift({
      value: String(props.value),
      label: `${props.value}(当前未检测到)`,
    });
  return (
    <Field>
      <FieldLabel htmlFor={id}>{label}</FieldLabel>
      <NativeSelect
        id={id}
        className="w-full"
        {...props}
        onChange={(e) => onValue(e.target.value)}
      >
        {optionsWithCurrent.map((o) => (
          <option key={o.value} value={o.value} disabled={o.disabled}>
            {o.label}
          </option>
        ))}
      </NativeSelect>
    </Field>
  );
}
export function CheckField({
  label,
  checked,
  onChange,
  disabled,
}: {
  label: string;
  checked: boolean;
  onChange: (value: boolean) => void;
  disabled?: boolean;
}) {
  const id = useId();
  return (
    <Field orientation="horizontal">
      <Checkbox
        id={id}
        checked={checked}
        onCheckedChange={(value) => onChange(value === true)}
        disabled={disabled}
      />
      <FieldLabel htmlFor={id}>{label}</FieldLabel>
    </Field>
  );
}
export function Section({
  title,
  hint,
  children,
}: {
  title: string;
  hint?: string;
  children: ReactNode;
}) {
  return (
    <section className="form-section">
      <div>
        <h2>{title}</h2>
        {hint && <p className="hint">{hint}</p>}
      </div>
      <FieldGroup className="form-fields">{children}</FieldGroup>
    </section>
  );
}
export function Notice({ children }: { children: ReactNode }) {
  return (
    <Alert>
      <AlertDescription>{children}</AlertDescription>
    </Alert>
  );
}
export function useAction() {
  const [pending, setPending] = useState(false);
  const [error, setError] = useState("");
  const running = useRef(false);
  async function run(action: () => Promise<unknown>, message?: string) {
    if (running.current) return false;
    running.current = true;
    setPending(true);
    setError("");
    try {
      await action();
      if (message) toast.success(message);
      return true;
    } catch (e) {
      const message = e instanceof Error ? e.message : String(e);
      setError(message);
      toast.error(message);
      return false;
    } finally {
      running.current = false;
      setPending(false);
    }
  }
  return { pending, error, run };
}
export function ConfirmAction({
  label,
  description,
  action,
  disabled,
  triggerVariant = "destructive",
}: {
  label: string;
  description: string;
  action: () => Promise<unknown>;
  disabled?: boolean;
  triggerVariant?: "ghost" | "destructive";
}) {
  const [open, setOpen] = useState(false);
  const task = useAction();
  return (
    <>
      <Button
        type="button"
        variant={triggerVariant}
        size="sm"
        disabled={disabled}
        onClick={() => setOpen(true)}
      >
        {label}
      </Button>
      <Dialog
        open={open}
        onOpenChange={(v) => {
          if (!task.pending) setOpen(v);
        }}
      >
        <DialogContent>
          <DialogTitle>{label}</DialogTitle>
          <DialogDescription>{description}</DialogDescription>
          {task.error && <Notice>{task.error}</Notice>}
          <DialogFooter>
            <Button
              variant="outline"
              onClick={() => setOpen(false)}
              disabled={task.pending}
            >
              取消
            </Button>
            <Button
              variant="destructive"
              disabled={task.pending}
              onClick={() =>
                void task.run(action, "操作已提交").then((ok) => {
                  if (ok) setOpen(false);
                })
              }
            >
              {task.pending ? "提交中…" : "确认"}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}
export function DataTable({
  headers,
  children,
  empty,
}: {
  headers: string[];
  children: ReactNode;
  empty?: boolean;
}) {
  if (empty)
    return (
      <Empty>
        <EmptyHeader>
          <EmptyTitle>暂无数据</EmptyTitle>
          <EmptyDescription>数据发生变化时会自动显示。</EmptyDescription>
        </EmptyHeader>
      </Empty>
    );
  return (
    <div className="table-frame">
      <Table>
        <TableHeader>
          <TableRow>
            {headers.map((h) => (
              <TableHead key={h}>{h}</TableHead>
            ))}
          </TableRow>
        </TableHeader>
        <TableBody>{children}</TableBody>
      </Table>
    </div>
  );
}
export { TableRow as Row, TableCell as Cell };
export const options = (values: string[]) =>
  values.map((value) => ({ value, label: value }));
export function ReadonlyInfo({
  entries,
}: {
  entries: Record<string, unknown>;
}) {
  return (
    <dl className="facts">
      {Object.entries(entries).map(([k, v]) => (
        <div key={k}>
          <dt>{k}</dt>
          <dd>{v === true ? "是" : v === false ? "否" : String(v ?? "—")}</dd>
        </div>
      ))}
    </dl>
  );
}