docs: add report draft and dashboard form refinements
This commit is contained in:
parent
4e0cf0140f
commit
441e709193
26 changed files with 1700 additions and 505 deletions
|
|
@ -58,7 +58,7 @@ export function DashboardShell({
|
|||
</p>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Operations overview for demos and day-to-day admin tasks</h1>
|
||||
<p className="max-w-2xl text-sm text-muted-foreground">
|
||||
Use this area to review monitored assets, inspect detail states, and stage CRUD forms that can connect to tRPC routers as they are added.
|
||||
Use this area to review monitored assets, inspect detail states, currently used to monitor my homelab setup.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3 border-l-0 border-t pt-4 lg:border-l lg:border-t-0 lg:pl-6 lg:pt-0">
|
||||
|
|
@ -91,7 +91,7 @@ export function DashboardShell({
|
|||
</div>
|
||||
<nav className="grid gap-1 p-2">
|
||||
{dashboardLinks.map(({ to, label, description, icon: Icon }) => {
|
||||
const active = pathname === to || pathname.startsWith(`${to}/`);
|
||||
const active = to === "/dashboard" ? pathname === to : pathname === to || pathname.startsWith(`${to}/`);
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ import {
|
|||
} from "@minmon/ui/components/card";
|
||||
import { Input } from "@minmon/ui/components/input";
|
||||
import { Label } from "@minmon/ui/components/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@minmon/ui/components/select";
|
||||
import { StatusBadge } from "@minmon/ui/components/status-badge";
|
||||
import {
|
||||
Table,
|
||||
|
|
@ -21,8 +28,8 @@ import {
|
|||
} from "@minmon/ui/components/table";
|
||||
import { Textarea } from "@minmon/ui/components/textarea";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Check, ChevronRight, Trash2 } from "lucide-react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
type BadgeVariant = "neutral" | "success" | "warning" | "destructive" | "info";
|
||||
|
||||
|
|
@ -66,7 +73,7 @@ export function SummaryCard({
|
|||
);
|
||||
}
|
||||
|
||||
export function ResourceListCard<TItem>({
|
||||
export function ResourceListCard<TItem extends { id: string }>({
|
||||
title,
|
||||
description,
|
||||
columns,
|
||||
|
|
@ -82,7 +89,7 @@ export function ResourceListCard<TItem>({
|
|||
emptyDescription: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className="gap-0">
|
||||
<Card className="py-0 pt-4 gap-0">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
|
|
@ -101,8 +108,8 @@ export function ResourceListCard<TItem>({
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item, index) => (
|
||||
<TableRow key={index}>
|
||||
{items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.key} className={column.className}>
|
||||
{column.render(item)}
|
||||
|
|
@ -213,6 +220,108 @@ export function TextAreaField(
|
|||
);
|
||||
}
|
||||
|
||||
export function SelectField({
|
||||
label,
|
||||
description,
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
onValueChange,
|
||||
onBlur,
|
||||
placeholder,
|
||||
options,
|
||||
}: {
|
||||
label: string;
|
||||
description?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
onBlur?: React.FocusEventHandler<HTMLButtonElement>;
|
||||
placeholder?: string;
|
||||
options: { value: string; label: string }[];
|
||||
}) {
|
||||
const fieldId = id ?? name ?? label;
|
||||
const selectedOption = options.find((option) => option.value === value);
|
||||
|
||||
return (
|
||||
<FieldGroup label={label} htmlFor={fieldId} description={description}>
|
||||
<Select name={name} value={value} onValueChange={(nextValue) => onValueChange(nextValue ?? "")}>
|
||||
<SelectTrigger id={fieldId} className="w-full" onBlur={onBlur}>
|
||||
<SelectValue placeholder={placeholder ?? "Select an option"}>
|
||||
{selectedOption?.label}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FieldGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConfirmDeleteIconButton({
|
||||
itemLabel,
|
||||
onConfirm,
|
||||
disabled,
|
||||
}: {
|
||||
itemLabel: string;
|
||||
onConfirm: () => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConfirming) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
setIsConfirming(false);
|
||||
}, 4000);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [isConfirming]);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setIsConfirming(false);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant={isConfirming ? "destructive" : "destructive"}
|
||||
size="icon"
|
||||
className="size-8"
|
||||
disabled={disabled}
|
||||
aria-label={isConfirming ? `Confirm delete ${itemLabel}` : `Delete ${itemLabel}`}
|
||||
onBlur={() => setIsConfirming(false)}
|
||||
onClick={() => {
|
||||
if (isConfirming) {
|
||||
onConfirm();
|
||||
setIsConfirming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConfirming(true);
|
||||
}}
|
||||
>
|
||||
{isConfirming ? <Check /> : <Trash2 />}
|
||||
<span className="sr-only">{isConfirming ? `Confirm delete ${itemLabel}` : `Delete ${itemLabel}`}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
title,
|
||||
description,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Button } from "@minmon/ui/components/button";
|
|||
import { Input } from "@minmon/ui/components/input";
|
||||
import { Label } from "@minmon/ui/components/label";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
import z from "zod";
|
||||
|
||||
|
|
@ -53,85 +53,84 @@ export default function SignInForm() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-10 grid w-full max-w-md gap-6 border bg-card p-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<p className="text-[11px] uppercase tracking-[0.24em] text-muted-foreground">Admin Access</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Sign in to minmon</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use your administrator account to open the protected monitoring dashboard.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div>
|
||||
<form.Field name="email">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Email</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="email"
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
{field.state.meta.errors.map((error) => (
|
||||
<p key={error?.message} className="text-xs text-red-500">
|
||||
{error?.message}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
<div className="mx-auto flex min-h-[calc(100dvh-8rem)] w-full max-w-5xl items-center justify-center px-4 py-10">
|
||||
<div className="grid w-full max-w-md gap-6 border bg-card p-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<p className="text-[11px] uppercase tracking-[0.24em] text-muted-foreground">Admin Access</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Sign in</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use your administrator account to open the protected monitoring dashboard.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form.Field name="password">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Password</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="password"
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
{field.state.meta.errors.map((error) => (
|
||||
<p key={error?.message} className="text-xs text-red-500">
|
||||
{error?.message}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
|
||||
<form.Subscribe
|
||||
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
className="space-y-4"
|
||||
>
|
||||
{({ canSubmit, isSubmitting }) => (
|
||||
<Button type="submit" className="w-full" disabled={!canSubmit || isSubmitting}>
|
||||
{isSubmitting ? "Submitting..." : "Sign In"}
|
||||
</Button>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
<div>
|
||||
<form.Field name="email">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Email</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="email"
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
{field.state.meta.errors.map((error) => (
|
||||
<p key={error?.message} className="text-xs text-red-500">
|
||||
{error?.message}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 border-t pt-4 text-center text-xs text-muted-foreground">
|
||||
<p>Student demo flow uses administrator sign-in only.</p>
|
||||
<Link to="/dashboard" className="text-primary underline-offset-4 hover:underline">
|
||||
Open dashboard after sign in
|
||||
</Link>
|
||||
<div>
|
||||
<form.Field name="password">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Password</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="password"
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
{field.state.meta.errors.map((error) => (
|
||||
<p key={error?.message} className="text-xs text-red-500">
|
||||
{error?.message}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
</div>
|
||||
|
||||
<form.Subscribe
|
||||
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
|
||||
>
|
||||
{({ canSubmit, isSubmitting }) => (
|
||||
<Button type="submit" className="w-full" disabled={!canSubmit || isSubmitting}>
|
||||
{isSubmitting ? "Submitting..." : "Sign In"}
|
||||
</Button>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
|
||||
<div className="grid gap-2 border-t pt-4 text-center text-xs text-muted-foreground">
|
||||
<p>Student demo flow uses administrator sign-in only.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ import { Route as DashboardServicesRouteImport } from './routes/dashboard.servic
|
|||
import { Route as DashboardServersRouteImport } from './routes/dashboard.servers'
|
||||
import { Route as DashboardNotesRouteImport } from './routes/dashboard.notes'
|
||||
import { Route as DashboardDomainsRouteImport } from './routes/dashboard.domains'
|
||||
import { Route as DashboardServicesNewRouteImport } from './routes/dashboard.services_.new'
|
||||
import { Route as DashboardServicesServiceIdRouteImport } from './routes/dashboard.services_.$serviceId'
|
||||
import { Route as DashboardServicesNewRouteImport } from './routes/dashboard.services.new'
|
||||
import { Route as DashboardServersNewRouteImport } from './routes/dashboard.servers_.new'
|
||||
import { Route as DashboardServersServerIdRouteImport } from './routes/dashboard.servers_.$serverId'
|
||||
import { Route as DashboardServersNewRouteImport } from './routes/dashboard.servers.new'
|
||||
import { Route as DashboardNotesNewRouteImport } from './routes/dashboard.notes_.new'
|
||||
import { Route as DashboardNotesNoteIdRouteImport } from './routes/dashboard.notes_.$noteId'
|
||||
import { Route as DashboardNotesNewRouteImport } from './routes/dashboard.notes.new'
|
||||
import { Route as DashboardDomainsNewRouteImport } from './routes/dashboard.domains_.new'
|
||||
import { Route as DashboardDomainsDomainIdRouteImport } from './routes/dashboard.domains_.$domainId'
|
||||
import { Route as DashboardDomainsNewRouteImport } from './routes/dashboard.domains.new'
|
||||
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
id: '/login',
|
||||
|
|
@ -66,16 +66,21 @@ const DashboardDomainsRoute = DashboardDomainsRouteImport.update({
|
|||
path: '/domains',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
const DashboardServicesNewRoute = DashboardServicesNewRouteImport.update({
|
||||
id: '/services_/new',
|
||||
path: '/services/new',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
const DashboardServicesServiceIdRoute =
|
||||
DashboardServicesServiceIdRouteImport.update({
|
||||
id: '/services_/$serviceId',
|
||||
path: '/services/$serviceId',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
const DashboardServicesNewRoute = DashboardServicesNewRouteImport.update({
|
||||
id: '/new',
|
||||
path: '/new',
|
||||
getParentRoute: () => DashboardServicesRoute,
|
||||
const DashboardServersNewRoute = DashboardServersNewRouteImport.update({
|
||||
id: '/servers_/new',
|
||||
path: '/servers/new',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
const DashboardServersServerIdRoute =
|
||||
DashboardServersServerIdRouteImport.update({
|
||||
|
|
@ -83,20 +88,20 @@ const DashboardServersServerIdRoute =
|
|||
path: '/servers/$serverId',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
const DashboardServersNewRoute = DashboardServersNewRouteImport.update({
|
||||
id: '/new',
|
||||
path: '/new',
|
||||
getParentRoute: () => DashboardServersRoute,
|
||||
const DashboardNotesNewRoute = DashboardNotesNewRouteImport.update({
|
||||
id: '/notes_/new',
|
||||
path: '/notes/new',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
const DashboardNotesNoteIdRoute = DashboardNotesNoteIdRouteImport.update({
|
||||
id: '/notes_/$noteId',
|
||||
path: '/notes/$noteId',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
const DashboardNotesNewRoute = DashboardNotesNewRouteImport.update({
|
||||
id: '/new',
|
||||
path: '/new',
|
||||
getParentRoute: () => DashboardNotesRoute,
|
||||
const DashboardDomainsNewRoute = DashboardDomainsNewRouteImport.update({
|
||||
id: '/domains_/new',
|
||||
path: '/domains/new',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
const DashboardDomainsDomainIdRoute =
|
||||
DashboardDomainsDomainIdRouteImport.update({
|
||||
|
|
@ -104,65 +109,60 @@ const DashboardDomainsDomainIdRoute =
|
|||
path: '/domains/$domainId',
|
||||
getParentRoute: () => DashboardRoute,
|
||||
} as any)
|
||||
const DashboardDomainsNewRoute = DashboardDomainsNewRouteImport.update({
|
||||
id: '/new',
|
||||
path: '/new',
|
||||
getParentRoute: () => DashboardDomainsRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/dashboard': typeof DashboardRouteWithChildren
|
||||
'/login': typeof LoginRoute
|
||||
'/dashboard/domains': typeof DashboardDomainsRouteWithChildren
|
||||
'/dashboard/notes': typeof DashboardNotesRouteWithChildren
|
||||
'/dashboard/servers': typeof DashboardServersRouteWithChildren
|
||||
'/dashboard/services': typeof DashboardServicesRouteWithChildren
|
||||
'/dashboard/domains': typeof DashboardDomainsRoute
|
||||
'/dashboard/notes': typeof DashboardNotesRoute
|
||||
'/dashboard/servers': typeof DashboardServersRoute
|
||||
'/dashboard/services': typeof DashboardServicesRoute
|
||||
'/dashboard/': typeof DashboardIndexRoute
|
||||
'/dashboard/domains/new': typeof DashboardDomainsNewRoute
|
||||
'/dashboard/domains/$domainId': typeof DashboardDomainsDomainIdRoute
|
||||
'/dashboard/notes/new': typeof DashboardNotesNewRoute
|
||||
'/dashboard/domains/new': typeof DashboardDomainsNewRoute
|
||||
'/dashboard/notes/$noteId': typeof DashboardNotesNoteIdRoute
|
||||
'/dashboard/servers/new': typeof DashboardServersNewRoute
|
||||
'/dashboard/notes/new': typeof DashboardNotesNewRoute
|
||||
'/dashboard/servers/$serverId': typeof DashboardServersServerIdRoute
|
||||
'/dashboard/services/new': typeof DashboardServicesNewRoute
|
||||
'/dashboard/servers/new': typeof DashboardServersNewRoute
|
||||
'/dashboard/services/$serviceId': typeof DashboardServicesServiceIdRoute
|
||||
'/dashboard/services/new': typeof DashboardServicesNewRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/dashboard/domains': typeof DashboardDomainsRouteWithChildren
|
||||
'/dashboard/notes': typeof DashboardNotesRouteWithChildren
|
||||
'/dashboard/servers': typeof DashboardServersRouteWithChildren
|
||||
'/dashboard/services': typeof DashboardServicesRouteWithChildren
|
||||
'/dashboard/domains': typeof DashboardDomainsRoute
|
||||
'/dashboard/notes': typeof DashboardNotesRoute
|
||||
'/dashboard/servers': typeof DashboardServersRoute
|
||||
'/dashboard/services': typeof DashboardServicesRoute
|
||||
'/dashboard': typeof DashboardIndexRoute
|
||||
'/dashboard/domains/new': typeof DashboardDomainsNewRoute
|
||||
'/dashboard/domains/$domainId': typeof DashboardDomainsDomainIdRoute
|
||||
'/dashboard/notes/new': typeof DashboardNotesNewRoute
|
||||
'/dashboard/domains/new': typeof DashboardDomainsNewRoute
|
||||
'/dashboard/notes/$noteId': typeof DashboardNotesNoteIdRoute
|
||||
'/dashboard/servers/new': typeof DashboardServersNewRoute
|
||||
'/dashboard/notes/new': typeof DashboardNotesNewRoute
|
||||
'/dashboard/servers/$serverId': typeof DashboardServersServerIdRoute
|
||||
'/dashboard/services/new': typeof DashboardServicesNewRoute
|
||||
'/dashboard/servers/new': typeof DashboardServersNewRoute
|
||||
'/dashboard/services/$serviceId': typeof DashboardServicesServiceIdRoute
|
||||
'/dashboard/services/new': typeof DashboardServicesNewRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/dashboard': typeof DashboardRouteWithChildren
|
||||
'/login': typeof LoginRoute
|
||||
'/dashboard/domains': typeof DashboardDomainsRouteWithChildren
|
||||
'/dashboard/notes': typeof DashboardNotesRouteWithChildren
|
||||
'/dashboard/servers': typeof DashboardServersRouteWithChildren
|
||||
'/dashboard/services': typeof DashboardServicesRouteWithChildren
|
||||
'/dashboard/domains': typeof DashboardDomainsRoute
|
||||
'/dashboard/notes': typeof DashboardNotesRoute
|
||||
'/dashboard/servers': typeof DashboardServersRoute
|
||||
'/dashboard/services': typeof DashboardServicesRoute
|
||||
'/dashboard/': typeof DashboardIndexRoute
|
||||
'/dashboard/domains/new': typeof DashboardDomainsNewRoute
|
||||
'/dashboard/domains_/$domainId': typeof DashboardDomainsDomainIdRoute
|
||||
'/dashboard/notes/new': typeof DashboardNotesNewRoute
|
||||
'/dashboard/domains_/new': typeof DashboardDomainsNewRoute
|
||||
'/dashboard/notes_/$noteId': typeof DashboardNotesNoteIdRoute
|
||||
'/dashboard/servers/new': typeof DashboardServersNewRoute
|
||||
'/dashboard/notes_/new': typeof DashboardNotesNewRoute
|
||||
'/dashboard/servers_/$serverId': typeof DashboardServersServerIdRoute
|
||||
'/dashboard/services/new': typeof DashboardServicesNewRoute
|
||||
'/dashboard/servers_/new': typeof DashboardServersNewRoute
|
||||
'/dashboard/services_/$serviceId': typeof DashboardServicesServiceIdRoute
|
||||
'/dashboard/services_/new': typeof DashboardServicesNewRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
|
|
@ -175,14 +175,14 @@ export interface FileRouteTypes {
|
|||
| '/dashboard/servers'
|
||||
| '/dashboard/services'
|
||||
| '/dashboard/'
|
||||
| '/dashboard/domains/new'
|
||||
| '/dashboard/domains/$domainId'
|
||||
| '/dashboard/notes/new'
|
||||
| '/dashboard/domains/new'
|
||||
| '/dashboard/notes/$noteId'
|
||||
| '/dashboard/servers/new'
|
||||
| '/dashboard/notes/new'
|
||||
| '/dashboard/servers/$serverId'
|
||||
| '/dashboard/services/new'
|
||||
| '/dashboard/servers/new'
|
||||
| '/dashboard/services/$serviceId'
|
||||
| '/dashboard/services/new'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
|
|
@ -192,14 +192,14 @@ export interface FileRouteTypes {
|
|||
| '/dashboard/servers'
|
||||
| '/dashboard/services'
|
||||
| '/dashboard'
|
||||
| '/dashboard/domains/new'
|
||||
| '/dashboard/domains/$domainId'
|
||||
| '/dashboard/notes/new'
|
||||
| '/dashboard/domains/new'
|
||||
| '/dashboard/notes/$noteId'
|
||||
| '/dashboard/servers/new'
|
||||
| '/dashboard/notes/new'
|
||||
| '/dashboard/servers/$serverId'
|
||||
| '/dashboard/services/new'
|
||||
| '/dashboard/servers/new'
|
||||
| '/dashboard/services/$serviceId'
|
||||
| '/dashboard/services/new'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
|
|
@ -210,14 +210,14 @@ export interface FileRouteTypes {
|
|||
| '/dashboard/servers'
|
||||
| '/dashboard/services'
|
||||
| '/dashboard/'
|
||||
| '/dashboard/domains/new'
|
||||
| '/dashboard/domains_/$domainId'
|
||||
| '/dashboard/notes/new'
|
||||
| '/dashboard/domains_/new'
|
||||
| '/dashboard/notes_/$noteId'
|
||||
| '/dashboard/servers/new'
|
||||
| '/dashboard/notes_/new'
|
||||
| '/dashboard/servers_/$serverId'
|
||||
| '/dashboard/services/new'
|
||||
| '/dashboard/servers_/new'
|
||||
| '/dashboard/services_/$serviceId'
|
||||
| '/dashboard/services_/new'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
|
|
@ -284,6 +284,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof DashboardDomainsRouteImport
|
||||
parentRoute: typeof DashboardRoute
|
||||
}
|
||||
'/dashboard/services_/new': {
|
||||
id: '/dashboard/services_/new'
|
||||
path: '/services/new'
|
||||
fullPath: '/dashboard/services/new'
|
||||
preLoaderRoute: typeof DashboardServicesNewRouteImport
|
||||
parentRoute: typeof DashboardRoute
|
||||
}
|
||||
'/dashboard/services_/$serviceId': {
|
||||
id: '/dashboard/services_/$serviceId'
|
||||
path: '/services/$serviceId'
|
||||
|
|
@ -291,12 +298,12 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof DashboardServicesServiceIdRouteImport
|
||||
parentRoute: typeof DashboardRoute
|
||||
}
|
||||
'/dashboard/services/new': {
|
||||
id: '/dashboard/services/new'
|
||||
path: '/new'
|
||||
fullPath: '/dashboard/services/new'
|
||||
preLoaderRoute: typeof DashboardServicesNewRouteImport
|
||||
parentRoute: typeof DashboardServicesRoute
|
||||
'/dashboard/servers_/new': {
|
||||
id: '/dashboard/servers_/new'
|
||||
path: '/servers/new'
|
||||
fullPath: '/dashboard/servers/new'
|
||||
preLoaderRoute: typeof DashboardServersNewRouteImport
|
||||
parentRoute: typeof DashboardRoute
|
||||
}
|
||||
'/dashboard/servers_/$serverId': {
|
||||
id: '/dashboard/servers_/$serverId'
|
||||
|
|
@ -305,12 +312,12 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof DashboardServersServerIdRouteImport
|
||||
parentRoute: typeof DashboardRoute
|
||||
}
|
||||
'/dashboard/servers/new': {
|
||||
id: '/dashboard/servers/new'
|
||||
path: '/new'
|
||||
fullPath: '/dashboard/servers/new'
|
||||
preLoaderRoute: typeof DashboardServersNewRouteImport
|
||||
parentRoute: typeof DashboardServersRoute
|
||||
'/dashboard/notes_/new': {
|
||||
id: '/dashboard/notes_/new'
|
||||
path: '/notes/new'
|
||||
fullPath: '/dashboard/notes/new'
|
||||
preLoaderRoute: typeof DashboardNotesNewRouteImport
|
||||
parentRoute: typeof DashboardRoute
|
||||
}
|
||||
'/dashboard/notes_/$noteId': {
|
||||
id: '/dashboard/notes_/$noteId'
|
||||
|
|
@ -319,12 +326,12 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof DashboardNotesNoteIdRouteImport
|
||||
parentRoute: typeof DashboardRoute
|
||||
}
|
||||
'/dashboard/notes/new': {
|
||||
id: '/dashboard/notes/new'
|
||||
path: '/new'
|
||||
fullPath: '/dashboard/notes/new'
|
||||
preLoaderRoute: typeof DashboardNotesNewRouteImport
|
||||
parentRoute: typeof DashboardNotesRoute
|
||||
'/dashboard/domains_/new': {
|
||||
id: '/dashboard/domains_/new'
|
||||
path: '/domains/new'
|
||||
fullPath: '/dashboard/domains/new'
|
||||
preLoaderRoute: typeof DashboardDomainsNewRouteImport
|
||||
parentRoute: typeof DashboardRoute
|
||||
}
|
||||
'/dashboard/domains_/$domainId': {
|
||||
id: '/dashboard/domains_/$domainId'
|
||||
|
|
@ -333,83 +340,39 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof DashboardDomainsDomainIdRouteImport
|
||||
parentRoute: typeof DashboardRoute
|
||||
}
|
||||
'/dashboard/domains/new': {
|
||||
id: '/dashboard/domains/new'
|
||||
path: '/new'
|
||||
fullPath: '/dashboard/domains/new'
|
||||
preLoaderRoute: typeof DashboardDomainsNewRouteImport
|
||||
parentRoute: typeof DashboardDomainsRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DashboardDomainsRouteChildren {
|
||||
interface DashboardRouteChildren {
|
||||
DashboardDomainsRoute: typeof DashboardDomainsRoute
|
||||
DashboardNotesRoute: typeof DashboardNotesRoute
|
||||
DashboardServersRoute: typeof DashboardServersRoute
|
||||
DashboardServicesRoute: typeof DashboardServicesRoute
|
||||
DashboardIndexRoute: typeof DashboardIndexRoute
|
||||
DashboardDomainsDomainIdRoute: typeof DashboardDomainsDomainIdRoute
|
||||
DashboardDomainsNewRoute: typeof DashboardDomainsNewRoute
|
||||
}
|
||||
|
||||
const DashboardDomainsRouteChildren: DashboardDomainsRouteChildren = {
|
||||
DashboardDomainsNewRoute: DashboardDomainsNewRoute,
|
||||
}
|
||||
|
||||
const DashboardDomainsRouteWithChildren =
|
||||
DashboardDomainsRoute._addFileChildren(DashboardDomainsRouteChildren)
|
||||
|
||||
interface DashboardNotesRouteChildren {
|
||||
DashboardNotesNoteIdRoute: typeof DashboardNotesNoteIdRoute
|
||||
DashboardNotesNewRoute: typeof DashboardNotesNewRoute
|
||||
}
|
||||
|
||||
const DashboardNotesRouteChildren: DashboardNotesRouteChildren = {
|
||||
DashboardNotesNewRoute: DashboardNotesNewRoute,
|
||||
}
|
||||
|
||||
const DashboardNotesRouteWithChildren = DashboardNotesRoute._addFileChildren(
|
||||
DashboardNotesRouteChildren,
|
||||
)
|
||||
|
||||
interface DashboardServersRouteChildren {
|
||||
DashboardServersServerIdRoute: typeof DashboardServersServerIdRoute
|
||||
DashboardServersNewRoute: typeof DashboardServersNewRoute
|
||||
}
|
||||
|
||||
const DashboardServersRouteChildren: DashboardServersRouteChildren = {
|
||||
DashboardServersNewRoute: DashboardServersNewRoute,
|
||||
}
|
||||
|
||||
const DashboardServersRouteWithChildren =
|
||||
DashboardServersRoute._addFileChildren(DashboardServersRouteChildren)
|
||||
|
||||
interface DashboardServicesRouteChildren {
|
||||
DashboardServicesServiceIdRoute: typeof DashboardServicesServiceIdRoute
|
||||
DashboardServicesNewRoute: typeof DashboardServicesNewRoute
|
||||
}
|
||||
|
||||
const DashboardServicesRouteChildren: DashboardServicesRouteChildren = {
|
||||
DashboardServicesNewRoute: DashboardServicesNewRoute,
|
||||
}
|
||||
|
||||
const DashboardServicesRouteWithChildren =
|
||||
DashboardServicesRoute._addFileChildren(DashboardServicesRouteChildren)
|
||||
|
||||
interface DashboardRouteChildren {
|
||||
DashboardDomainsRoute: typeof DashboardDomainsRouteWithChildren
|
||||
DashboardNotesRoute: typeof DashboardNotesRouteWithChildren
|
||||
DashboardServersRoute: typeof DashboardServersRouteWithChildren
|
||||
DashboardServicesRoute: typeof DashboardServicesRouteWithChildren
|
||||
DashboardIndexRoute: typeof DashboardIndexRoute
|
||||
DashboardDomainsDomainIdRoute: typeof DashboardDomainsDomainIdRoute
|
||||
DashboardNotesNoteIdRoute: typeof DashboardNotesNoteIdRoute
|
||||
DashboardServersServerIdRoute: typeof DashboardServersServerIdRoute
|
||||
DashboardServicesServiceIdRoute: typeof DashboardServicesServiceIdRoute
|
||||
}
|
||||
|
||||
const DashboardRouteChildren: DashboardRouteChildren = {
|
||||
DashboardDomainsRoute: DashboardDomainsRouteWithChildren,
|
||||
DashboardNotesRoute: DashboardNotesRouteWithChildren,
|
||||
DashboardServersRoute: DashboardServersRouteWithChildren,
|
||||
DashboardServicesRoute: DashboardServicesRouteWithChildren,
|
||||
DashboardDomainsRoute: DashboardDomainsRoute,
|
||||
DashboardNotesRoute: DashboardNotesRoute,
|
||||
DashboardServersRoute: DashboardServersRoute,
|
||||
DashboardServicesRoute: DashboardServicesRoute,
|
||||
DashboardIndexRoute: DashboardIndexRoute,
|
||||
DashboardDomainsDomainIdRoute: DashboardDomainsDomainIdRoute,
|
||||
DashboardDomainsNewRoute: DashboardDomainsNewRoute,
|
||||
DashboardNotesNoteIdRoute: DashboardNotesNoteIdRoute,
|
||||
DashboardNotesNewRoute: DashboardNotesNewRoute,
|
||||
DashboardServersServerIdRoute: DashboardServersServerIdRoute,
|
||||
DashboardServersNewRoute: DashboardServersNewRoute,
|
||||
DashboardServicesServiceIdRoute: DashboardServicesServiceIdRoute,
|
||||
DashboardServicesNewRoute: DashboardServicesNewRoute,
|
||||
}
|
||||
|
||||
const DashboardRouteWithChildren = DashboardRoute._addFileChildren(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Button } from "@minmon/ui/components/button";
|
||||
import { StatusBadge } from "@minmon/ui/components/status-badge";
|
||||
import { statusBadgeVariants } from "@minmon/ui/components/status-badge";
|
||||
import { cn } from "@minmon/ui/lib/utils";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
|
||||
import { ConfirmDeleteIconButton, PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/domains")({
|
||||
|
|
@ -93,38 +94,29 @@ function RouteComponent() {
|
|||
header: "DNS status",
|
||||
className: "w-36",
|
||||
render: (item: (typeof domains.data)[number]) => (
|
||||
<StatusBadge variant={dnsStatusVariant(item.resolutionStatus)}>{item.resolutionStatus}</StatusBadge>
|
||||
<button
|
||||
type="button"
|
||||
disabled={checkDns.isPending}
|
||||
className={cn(
|
||||
statusBadgeVariants({ variant: dnsStatusVariant(item.resolutionStatus) }),
|
||||
"cursor-pointer transition-opacity hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-60",
|
||||
)}
|
||||
onClick={() => checkDns.mutate(item.id)}
|
||||
>
|
||||
{item.resolutionStatus}
|
||||
</button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
header: "Actions",
|
||||
className: "w-44",
|
||||
className: "w-16",
|
||||
render: (item: (typeof domains.data)[number]) => (
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-auto px-0"
|
||||
disabled={checkDns.isPending}
|
||||
onClick={() => checkDns.mutate(item.id)}
|
||||
>
|
||||
Check DNS
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-auto px-0 text-red-600 hover:text-red-700"
|
||||
disabled={removeDomain.isPending}
|
||||
onClick={() => {
|
||||
if (confirm(`Delete domain \"${item.name}\"?`)) {
|
||||
removeDomain.mutate(item.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
<ConfirmDeleteIconButton
|
||||
itemLabel={`domain ${item.name}`}
|
||||
disabled={removeDomain.isPending}
|
||||
onConfirm={() => removeDomain.mutate(item.id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { DetailCard, FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { DetailCard, FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
import { Button } from "@minmon/ui/components/button";
|
||||
import { StatusBadge } from "@minmon/ui/components/status-badge";
|
||||
|
|
@ -88,6 +88,11 @@ function RouteComponent() {
|
|||
return <QueryStateCard title="Domain unavailable" description="The requested domain could not be loaded." />;
|
||||
}
|
||||
|
||||
const serviceOptions = services.data.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} (${item.id})`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="grid gap-6">
|
||||
<PageSection title={domain.data.name} description="View DNS check results and edit this domain record.">
|
||||
|
|
@ -134,16 +139,18 @@ function RouteComponent() {
|
|||
>
|
||||
<form.Field name="serviceId">
|
||||
{(field) => (
|
||||
<TextField
|
||||
label="Service ID"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
description={`Available: ${services.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
<SelectField
|
||||
label="Service"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onValueChange={field.handleChange}
|
||||
description="Select the service this domain belongs to."
|
||||
placeholder="Select a service"
|
||||
options={serviceOptions}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<form.Field name="name">{(field) => <TextField label="Domain name" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
<form.Field name="expectedServerIp">{(field) => <TextField label="Expected server IP" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { useForm } from "@tanstack/react-form";
|
|||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
import { FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/domains/new")({
|
||||
export const Route = createFileRoute("/dashboard/domains_/new")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
|
|
@ -58,6 +58,11 @@ function RouteComponent() {
|
|||
return <QueryStateCard title="Service options unavailable" description="Create a service before adding domains." />;
|
||||
}
|
||||
|
||||
const serviceOptions = services.data.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} (${item.id})`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<PageSection title="Add domain" description="Create a domain or subdomain attached to a service.">
|
||||
<form
|
||||
|
|
@ -84,13 +89,15 @@ function RouteComponent() {
|
|||
>
|
||||
<form.Field name="serviceId">
|
||||
{(field) => (
|
||||
<TextField
|
||||
label="Service ID"
|
||||
<SelectField
|
||||
label="Service"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
description={`Available: ${services.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
|
||||
onValueChange={field.handleChange}
|
||||
description="Select the service this domain belongs to."
|
||||
placeholder="Select a service"
|
||||
options={serviceOptions}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
|
|
@ -3,7 +3,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
|||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
|
||||
import { ConfirmDeleteIconButton, PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/notes")({
|
||||
|
|
@ -76,21 +76,13 @@ function RouteComponent() {
|
|||
{
|
||||
key: "actions",
|
||||
header: "Actions",
|
||||
className: "w-28",
|
||||
className: "w-16",
|
||||
render: (item: (typeof notes.data)[number]) => (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-auto px-0 text-red-600 hover:text-red-700"
|
||||
<ConfirmDeleteIconButton
|
||||
itemLabel={`note ${item.title}`}
|
||||
disabled={removeNote.isPending}
|
||||
onClick={() => {
|
||||
if (confirm(`Delete note \"${item.title}\"?`)) {
|
||||
removeNote.mutate(item.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
onConfirm={() => removeNote.mutate(item.id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
|||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DetailCard, FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { DetailCard, FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/notes_/$noteId")({
|
||||
|
|
@ -75,6 +75,12 @@ function RouteComponent() {
|
|||
return <QueryStateCard title="Note unavailable" description="The requested note could not be loaded." />;
|
||||
}
|
||||
|
||||
const serverMap = new Map(servers.data.map((item) => [item.id, item.name]));
|
||||
const serverOptions = servers.data.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} (${item.id})`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="grid gap-6">
|
||||
<PageSection title={note.data.title} description="View and edit this note.">
|
||||
|
|
@ -82,7 +88,7 @@ function RouteComponent() {
|
|||
title="Note summary"
|
||||
description="Current note metadata and content preview."
|
||||
rows={[
|
||||
{ label: "Server ID", value: note.data.serverId },
|
||||
{ label: "Server", value: serverMap.get(note.data.serverId) || note.data.serverId },
|
||||
{ label: "Category", value: note.data.category || "-" },
|
||||
{ label: "Updated", value: new Date(note.data.updatedAt).toLocaleString() },
|
||||
{ label: "Content", value: note.data.content },
|
||||
|
|
@ -114,13 +120,15 @@ function RouteComponent() {
|
|||
>
|
||||
<form.Field name="serverId">
|
||||
{(field) => (
|
||||
<TextField
|
||||
label="Server ID"
|
||||
<SelectField
|
||||
label="Server"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
description={`Available: ${servers.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
|
||||
onValueChange={field.handleChange}
|
||||
description="Select the server this note belongs to."
|
||||
placeholder="Select a server"
|
||||
options={serverOptions}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { useForm } from "@tanstack/react-form";
|
|||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
import { FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/notes/new")({
|
||||
export const Route = createFileRoute("/dashboard/notes_/new")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
|
|
@ -58,6 +58,11 @@ function RouteComponent() {
|
|||
return <QueryStateCard title="Server options unavailable" description="Add or load servers before creating notes." />;
|
||||
}
|
||||
|
||||
const serverOptions = servers.data.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} (${item.id})`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<PageSection title="New note" description="Add documentation or operational notes for a server.">
|
||||
<form
|
||||
|
|
@ -84,13 +89,15 @@ function RouteComponent() {
|
|||
>
|
||||
<form.Field name="serverId">
|
||||
{(field) => (
|
||||
<TextField
|
||||
label="Server ID"
|
||||
<SelectField
|
||||
label="Server"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
description={`Available: ${servers.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
|
||||
onValueChange={field.handleChange}
|
||||
description="Select the server this note belongs to."
|
||||
placeholder="Select a server"
|
||||
options={serverOptions}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
|
|
@ -4,7 +4,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
|||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
|
||||
import { ConfirmDeleteIconButton, PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/servers")({
|
||||
|
|
@ -99,21 +99,13 @@ function RouteComponent() {
|
|||
{
|
||||
key: "actions",
|
||||
header: "Actions",
|
||||
className: "w-28",
|
||||
className: "w-16",
|
||||
render: (item: (typeof servers.data)[number]) => (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-auto px-0 text-red-600 hover:text-red-700"
|
||||
<ConfirmDeleteIconButton
|
||||
itemLabel={`server ${item.name}`}
|
||||
disabled={removeServer.isPending}
|
||||
onClick={() => {
|
||||
if (confirm(`Delete server \"${item.name}\" and all related services, domains, and notes?`)) {
|
||||
removeServer.mutate(item.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
onConfirm={() => removeServer.mutate(item.id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { Button } from "@minmon/ui/components/button";
|
|||
import { StatusBadge } from "@minmon/ui/components/status-badge";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DetailCard, FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { DetailCard, FormCard, PageSection, QueryStateCard, ResourceListCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/servers_/$serverId")({
|
||||
|
|
@ -23,10 +23,17 @@ type ServerFormValues = {
|
|||
status: "active" | "maintenance" | "inactive";
|
||||
};
|
||||
|
||||
const serverStatusOptions = [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "maintenance", label: "Maintenance" },
|
||||
{ value: "inactive", label: "Inactive" },
|
||||
] as const;
|
||||
|
||||
function RouteComponent() {
|
||||
const { serverId } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const server = useQuery(trpc.servers.byId.queryOptions({ id: serverId }));
|
||||
const notes = useQuery(trpc.notes.list.queryOptions({ serverId }));
|
||||
|
||||
const updateServer = useMutation({
|
||||
mutationFn: async (values: ServerFormValues) =>
|
||||
|
|
@ -108,6 +115,54 @@ function RouteComponent() {
|
|||
/>
|
||||
</PageSection>
|
||||
|
||||
{notes.isLoading ? (
|
||||
<QueryStateCard title="Loading server notes" description="Fetching notes attached to this server." />
|
||||
) : notes.isError || !notes.data ? (
|
||||
<QueryStateCard title="Server notes unavailable" description="The notes for this server could not be loaded." />
|
||||
) : (
|
||||
<PageSection
|
||||
title="Server notes"
|
||||
description="Notes and internal documentation attached to this server."
|
||||
action={
|
||||
<Link to="/dashboard/notes/new">
|
||||
<Button variant="outline">New note</Button>
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<ResourceListCard
|
||||
title="Attached notes"
|
||||
description="Review server-specific documentation without leaving this detail page."
|
||||
columns={[
|
||||
{
|
||||
key: "title",
|
||||
header: "Title",
|
||||
render: (item: (typeof notes.data)[number]) => (
|
||||
<div className="grid gap-1">
|
||||
<Link to="/dashboard/notes/$noteId" params={{ noteId: item.id }} className="font-medium hover:underline">
|
||||
{item.title}
|
||||
</Link>
|
||||
<span className="text-xs text-muted-foreground line-clamp-2">{item.content}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "category",
|
||||
header: "Category",
|
||||
render: (item: (typeof notes.data)[number]) => item.category || "-",
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
header: "Updated",
|
||||
render: (item: (typeof notes.data)[number]) => new Date(item.updatedAt).toLocaleString(),
|
||||
},
|
||||
]}
|
||||
items={notes.data}
|
||||
emptyTitle="No notes for this server"
|
||||
emptyDescription="Add a note to document maintenance, incidents, or internal server context."
|
||||
/>
|
||||
</PageSection>
|
||||
)}
|
||||
|
||||
<form
|
||||
className="grid gap-4"
|
||||
onSubmit={(e) => {
|
||||
|
|
@ -137,7 +192,7 @@ function RouteComponent() {
|
|||
<form.Field name="operatingSystem">{(field) => <TextField label="Operating system" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
<form.Field name="location">{(field) => <TextField label="Location" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
<form.Field name="provider">{(field) => <TextField label="Provider" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
<form.Field name="status">{(field) => <TextField label="Status" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value as "active" | "maintenance" | "inactive")} />}</form.Field>
|
||||
<form.Field name="status">{(field) => <SelectField label="Status" name={field.name} value={field.state.value} onBlur={field.handleBlur} onValueChange={(value) => field.handleChange(value as ServerFormValues["status"])} placeholder="Select server status" options={[...serverStatusOptions]} />}</form.Field>
|
||||
</div>
|
||||
<form.Field name="description">{(field) => <TextAreaField label="Description" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
</FormCard>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { useForm } from "@tanstack/react-form";
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
import { FormCard, PageSection, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { FormCard, PageSection, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/servers/new")({
|
||||
export const Route = createFileRoute("/dashboard/servers_/new")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
|
|
@ -21,6 +21,12 @@ type ServerFormValues = {
|
|||
status: "active" | "maintenance" | "inactive";
|
||||
};
|
||||
|
||||
const serverStatusOptions = [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "maintenance", label: "Maintenance" },
|
||||
{ value: "inactive", label: "Inactive" },
|
||||
] as const;
|
||||
|
||||
function RouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const createServer = useMutation({
|
||||
|
|
@ -160,13 +166,14 @@ function RouteComponent() {
|
|||
</form.Field>
|
||||
<form.Field name="status">
|
||||
{(field) => (
|
||||
<TextField
|
||||
<SelectField
|
||||
label="Status"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value as "active" | "maintenance" | "inactive")}
|
||||
placeholder="active"
|
||||
onValueChange={(value) => field.handleChange(value as ServerFormValues["status"])}
|
||||
placeholder="Select server status"
|
||||
options={[...serverStatusOptions]}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
|
|
@ -4,7 +4,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
|||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
|
||||
import { ConfirmDeleteIconButton, PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/services")({
|
||||
|
|
@ -96,21 +96,13 @@ function RouteComponent() {
|
|||
{
|
||||
key: "actions",
|
||||
header: "Actions",
|
||||
className: "w-28",
|
||||
className: "w-16",
|
||||
render: (item: (typeof services.data)[number]) => (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-auto px-0 text-red-600 hover:text-red-700"
|
||||
<ConfirmDeleteIconButton
|
||||
itemLabel={`service ${item.name}`}
|
||||
disabled={removeService.isPending}
|
||||
onClick={() => {
|
||||
if (confirm(`Delete service \"${item.name}\" and its related domains?`)) {
|
||||
removeService.mutate(item.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
onConfirm={() => removeService.mutate(item.id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
|||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DetailCard, FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { DetailCard, FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/services_/$serviceId")({
|
||||
|
|
@ -23,6 +23,29 @@ type ServiceFormValues = {
|
|||
description: string;
|
||||
};
|
||||
|
||||
const serviceTypeOptions = [
|
||||
{ value: "web_app", label: "Web app" },
|
||||
{ value: "mail", label: "Mail" },
|
||||
{ value: "database", label: "Database" },
|
||||
{ value: "api", label: "API" },
|
||||
{ value: "proxy", label: "Proxy" },
|
||||
{ value: "other", label: "Other" },
|
||||
] as const;
|
||||
|
||||
const serviceProtocolOptions = [
|
||||
{ value: "http", label: "HTTP" },
|
||||
{ value: "https", label: "HTTPS" },
|
||||
{ value: "tcp", label: "TCP" },
|
||||
{ value: "udp", label: "UDP" },
|
||||
{ value: "other", label: "Other" },
|
||||
] as const;
|
||||
|
||||
const serviceStatusOptions = [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "down", label: "Down" },
|
||||
{ value: "maintenance", label: "Maintenance" },
|
||||
] as const;
|
||||
|
||||
function RouteComponent() {
|
||||
const { serviceId } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -92,6 +115,11 @@ function RouteComponent() {
|
|||
return <QueryStateCard title="Service unavailable" description="The requested service could not be loaded." />;
|
||||
}
|
||||
|
||||
const serverOptions = servers.data.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} (${item.id})`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="grid gap-6">
|
||||
<PageSection title={service.data.name} description="View and edit the selected service.">
|
||||
|
|
@ -134,23 +162,25 @@ function RouteComponent() {
|
|||
>
|
||||
<form.Field name="serverId">
|
||||
{(field) => (
|
||||
<TextField
|
||||
label="Server ID"
|
||||
<SelectField
|
||||
label="Server"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
description={`Available: ${servers.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
|
||||
onValueChange={field.handleChange}
|
||||
description="Select the server that owns this service."
|
||||
placeholder="Select a server"
|
||||
options={serverOptions}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<form.Field name="name">{(field) => <TextField label="Service name" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
<form.Field name="type">{(field) => <TextField label="Service type" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value as ServiceFormValues["type"])} />}</form.Field>
|
||||
<form.Field name="type">{(field) => <SelectField label="Service type" name={field.name} value={field.state.value} onBlur={field.handleBlur} onValueChange={(value) => field.handleChange(value as ServiceFormValues["type"])} placeholder="Select service type" options={[...serviceTypeOptions]} />}</form.Field>
|
||||
<form.Field name="internalPort">{(field) => <TextField label="Internal port" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
<form.Field name="externalPort">{(field) => <TextField label="External port" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
<form.Field name="protocol">{(field) => <TextField label="Protocol" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value as ServiceFormValues["protocol"])} />}</form.Field>
|
||||
<form.Field name="status">{(field) => <TextField label="Status" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value as ServiceFormValues["status"])} />}</form.Field>
|
||||
<form.Field name="protocol">{(field) => <SelectField label="Protocol" name={field.name} value={field.state.value} onBlur={field.handleBlur} onValueChange={(value) => field.handleChange(value as ServiceFormValues["protocol"])} placeholder="Select protocol" options={[...serviceProtocolOptions]} />}</form.Field>
|
||||
<form.Field name="status">{(field) => <SelectField label="Status" name={field.name} value={field.state.value} onBlur={field.handleBlur} onValueChange={(value) => field.handleChange(value as ServiceFormValues["status"])} placeholder="Select service status" options={[...serviceStatusOptions]} />}</form.Field>
|
||||
</div>
|
||||
<form.Field name="description">{(field) => <TextAreaField label="Description" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
</FormCard>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { useForm } from "@tanstack/react-form";
|
|||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { toast } from "sonner";
|
||||
import { FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
|
||||
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/services/new")({
|
||||
export const Route = createFileRoute("/dashboard/services_/new")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
|
|
@ -21,6 +21,29 @@ type ServiceFormValues = {
|
|||
description: string;
|
||||
};
|
||||
|
||||
const serviceTypeOptions = [
|
||||
{ value: "web_app", label: "Web app" },
|
||||
{ value: "mail", label: "Mail" },
|
||||
{ value: "database", label: "Database" },
|
||||
{ value: "api", label: "API" },
|
||||
{ value: "proxy", label: "Proxy" },
|
||||
{ value: "other", label: "Other" },
|
||||
] as const;
|
||||
|
||||
const serviceProtocolOptions = [
|
||||
{ value: "http", label: "HTTP" },
|
||||
{ value: "https", label: "HTTPS" },
|
||||
{ value: "tcp", label: "TCP" },
|
||||
{ value: "udp", label: "UDP" },
|
||||
{ value: "other", label: "Other" },
|
||||
] as const;
|
||||
|
||||
const serviceStatusOptions = [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "down", label: "Down" },
|
||||
{ value: "maintenance", label: "Maintenance" },
|
||||
] as const;
|
||||
|
||||
function RouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const servers = useQuery(trpc.servers.list.queryOptions());
|
||||
|
|
@ -70,6 +93,11 @@ function RouteComponent() {
|
|||
return <QueryStateCard title="Server options unavailable" description="Add or load servers before creating a service." />;
|
||||
}
|
||||
|
||||
const serverOptions = servers.data.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} (${item.id})`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<PageSection title="Add service" description="Create a new service under an existing server.">
|
||||
<form
|
||||
|
|
@ -96,23 +124,25 @@ function RouteComponent() {
|
|||
>
|
||||
<form.Field name="serverId">
|
||||
{(field) => (
|
||||
<TextField
|
||||
label="Server ID"
|
||||
<SelectField
|
||||
label="Server"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
description={`Available: ${servers.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
|
||||
onValueChange={field.handleChange}
|
||||
description="Select the server that owns this service."
|
||||
placeholder="Select a server"
|
||||
options={serverOptions}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<form.Field name="name">{(field) => <TextField label="Service name" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
<form.Field name="type">{(field) => <TextField label="Service type" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value as ServiceFormValues["type"])} />}</form.Field>
|
||||
<form.Field name="type">{(field) => <SelectField label="Service type" name={field.name} value={field.state.value} onBlur={field.handleBlur} onValueChange={(value) => field.handleChange(value as ServiceFormValues["type"])} placeholder="Select service type" options={[...serviceTypeOptions]} />}</form.Field>
|
||||
<form.Field name="internalPort">{(field) => <TextField label="Internal port" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
<form.Field name="externalPort">{(field) => <TextField label="External port" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
<form.Field name="protocol">{(field) => <TextField label="Protocol" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value as ServiceFormValues["protocol"])} />}</form.Field>
|
||||
<form.Field name="status">{(field) => <TextField label="Status" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value as ServiceFormValues["status"])} />}</form.Field>
|
||||
<form.Field name="protocol">{(field) => <SelectField label="Protocol" name={field.name} value={field.state.value} onBlur={field.handleBlur} onValueChange={(value) => field.handleChange(value as ServiceFormValues["protocol"])} placeholder="Select protocol" options={[...serviceProtocolOptions]} />}</form.Field>
|
||||
<form.Field name="status">{(field) => <SelectField label="Status" name={field.name} value={field.state.value} onBlur={field.handleBlur} onValueChange={(value) => field.handleChange(value as ServiceFormValues["status"])} placeholder="Select service status" options={[...serviceStatusOptions]} />}</form.Field>
|
||||
</div>
|
||||
<form.Field name="description">{(field) => <TextAreaField label="Description" name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} />}</form.Field>
|
||||
</FormCard>
|
||||
Loading…
Add table
Add a link
Reference in a new issue