From f47f1be6a52ddc7a0c96b020c0f187d4e2889b86 Mon Sep 17 00:00:00 2001 From: Richard Anderson Date: Fri, 29 May 2026 08:30:03 +0100 Subject: [PATCH 1/8] wip --- .../js/components/dialogs/dialog-host.tsx | 32 ++++++++++++++ .../components/dialogs/log-viewer-dialog.tsx | 39 ++++++++++++++++ resources/js/components/dialogs/registry.ts | 24 ++++++++++ resources/js/hooks/use-dialog.ts | 27 ++++++++++++ resources/js/layouts/app/layout.tsx | 6 ++- .../pages/server-logs/components/columns.tsx | 37 +++------------- .../pages/server-ssls/components/columns.tsx | 4 +- .../services/components/installation-log.tsx | 44 ++++--------------- resources/js/stores/dialog-store.ts | 21 +++++++++ 9 files changed, 163 insertions(+), 71 deletions(-) create mode 100644 resources/js/components/dialogs/dialog-host.tsx create mode 100644 resources/js/components/dialogs/log-viewer-dialog.tsx create mode 100644 resources/js/components/dialogs/registry.ts create mode 100644 resources/js/hooks/use-dialog.ts create mode 100644 resources/js/stores/dialog-store.ts diff --git a/resources/js/components/dialogs/dialog-host.tsx b/resources/js/components/dialogs/dialog-host.tsx new file mode 100644 index 000000000..99cbfa50c --- /dev/null +++ b/resources/js/components/dialogs/dialog-host.tsx @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; +import type { ComponentType } from 'react'; +import { router } from '@inertiajs/react'; +import { useDialogStore } from '@/stores/dialog-store'; +import { dialogs, type DialogControlProps } from './registry'; + +export default function DialogHost() { + const active = useDialogStore((s) => s.active); + + useEffect(() => { + return router.on('start', () => useDialogStore.getState().close()); + }, []); + + if (!active) { + return null; + } + + const Component = dialogs[active.key] as ComponentType | undefined; + + if (!Component) { + return null; + } + + return ( + !o && useDialogStore.getState().close()} + {...active.props} + /> + ); +} diff --git a/resources/js/components/dialogs/log-viewer-dialog.tsx b/resources/js/components/dialogs/log-viewer-dialog.tsx new file mode 100644 index 000000000..c92165f7e --- /dev/null +++ b/resources/js/components/dialogs/log-viewer-dialog.tsx @@ -0,0 +1,39 @@ +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import LogOutput from '@/components/log-output'; +import { useLogContent } from '@/hooks/use-log-content'; + +type LogViewerDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + serverId: number; + logId: number; + title: string; +}; + +export default function LogViewerDialog({ open, onOpenChange, serverId, logId, title }: LogViewerDialogProps) { + const { content, isLoading, error } = useLogContent({ serverId, logId, enabled: open }); + + return ( + + + + {title} + Log contents + + + <> + {isLoading && 'Loading...'} + {error &&
Error: {error}
} + {content && !error && content} + +
+ + + + + +
+
+ ); +} diff --git a/resources/js/components/dialogs/registry.ts b/resources/js/components/dialogs/registry.ts new file mode 100644 index 000000000..4066964d4 --- /dev/null +++ b/resources/js/components/dialogs/registry.ts @@ -0,0 +1,24 @@ +import type { ComponentType } from 'react'; +import LogViewerDialog from './log-viewer-dialog'; + +export type DialogControlProps = { open: boolean; onOpenChange: (open: boolean) => void }; + +/** + * Registry of all app-level dialogs that can be opened via `useDialog()`. + * + * Authorization contract: every registered consumer is responsible for + * ensuring its props come from server-authorised sources (Inertia page + * props, API resources, etc.), not from URL parameters or other + * user-controlled input. The registry pattern itself carries no authz. + * + * To register a new dialog: add one entry here, mapping a typed key to the + * component. Consumers immediately gain `dialog..open(props)` with + * full IntelliSense for the prop shape. + */ +export const dialogs = { + logViewer: LogViewerDialog, +} as const satisfies Record>; + +export type DialogRegistry = typeof dialogs; + +export type ConsumerProps = C extends ComponentType ? Omit : never; diff --git a/resources/js/hooks/use-dialog.ts b/resources/js/hooks/use-dialog.ts new file mode 100644 index 000000000..1ac142288 --- /dev/null +++ b/resources/js/hooks/use-dialog.ts @@ -0,0 +1,27 @@ +import { useMemo } from 'react'; +import { useDialogStore } from '@/stores/dialog-store'; +import { dialogs, type DialogRegistry, type ConsumerProps } from '@/components/dialogs/registry'; + +type DialogAccessor = { + -readonly [K in keyof DialogRegistry]: { + open: (props: ConsumerProps) => void; + close: () => void; + }; +}; + +function entryFor(key: K) { + return { + open: (props: ConsumerProps) => useDialogStore.getState().open(key, props), + close: () => useDialogStore.getState().close(), + }; +} + +export function useDialog(): DialogAccessor { + return useMemo(() => { + const accessor = {} as DialogAccessor; + (Object.keys(dialogs) as Array).forEach((key) => { + accessor[key] = entryFor(key) as DialogAccessor[typeof key]; + }); + return accessor; + }, []); +} diff --git a/resources/js/layouts/app/layout.tsx b/resources/js/layouts/app/layout.tsx index 0232b0ace..c09176e52 100644 --- a/resources/js/layouts/app/layout.tsx +++ b/resources/js/layouts/app/layout.tsx @@ -13,6 +13,7 @@ import { type SocketEventData, useSocketEvents, useSocketListener } from '@/hook import { useBootstrapStore } from '@/stores/bootstrap-store'; import { Button } from '@/components/ui/button'; import { AlertCircleIcon } from 'lucide-react'; +import DialogHost from '@/components/dialogs/dialog-host'; export default function Layout({ children, @@ -101,7 +102,10 @@ export default function Layout({ ) : bootstrapConfigsLoaded ? ( - children + <> + {children} + + ) : null} diff --git a/resources/js/pages/server-logs/components/columns.tsx b/resources/js/pages/server-logs/components/columns.tsx index a9441f68a..b93a4a8bb 100644 --- a/resources/js/pages/server-logs/components/columns.tsx +++ b/resources/js/pages/server-logs/components/columns.tsx @@ -14,43 +14,18 @@ import { } from '@/components/ui/dialog'; import { ReactNode, useState } from 'react'; import DateTime from '@/components/date-time'; -import LogOutput from '@/components/log-output'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { useForm } from '@inertiajs/react'; import FormSuccessful from '@/components/form-successful'; -import { useLogContent } from '@/hooks/use-log-content'; +import { useDialog } from '@/hooks/use-dialog'; -export function View({ serverLog, children }: { serverLog: ServerLog; children?: ReactNode }) { - const [open, setOpen] = useState(false); - - const { content, isLoading, error } = useLogContent({ - serverId: serverLog.server_id, - logId: serverLog.id, - enabled: open, - }); +export function View({ serverLog, label = 'View' }: { serverLog: ServerLog; label?: string }) { + const dialog = useDialog(); return ( - - {children ? children : e.preventDefault()}>View} - - - View Log - This is all content of the log - - - <> - {isLoading && 'Loading...'} - {error &&
Error: {error}
} - {content && !error && content} - -
- - - - - -
-
+ dialog.logViewer.open({ serverId: serverLog.server_id, logId: serverLog.id, title: label })}> + {label} + ); } diff --git a/resources/js/pages/server-ssls/components/columns.tsx b/resources/js/pages/server-ssls/components/columns.tsx index 8f71279ff..55764b371 100644 --- a/resources/js/pages/server-ssls/components/columns.tsx +++ b/resources/js/pages/server-ssls/components/columns.tsx @@ -195,9 +195,7 @@ export const columns: ColumnDef[] = [ )} {row.original.log && ( <> - - e.preventDefault()}>View Log - + )} diff --git a/resources/js/pages/services/components/installation-log.tsx b/resources/js/pages/services/components/installation-log.tsx index 578d5ae9b..19e182162 100644 --- a/resources/js/pages/services/components/installation-log.tsx +++ b/resources/js/pages/services/components/installation-log.tsx @@ -1,47 +1,19 @@ -import { Button } from '@/components/ui/button'; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { DropdownMenuItem } from '@/components/ui/dropdown-menu'; -import LogOutput from '@/components/log-output'; import { Service } from '@/types/service'; -import { ReactNode, useState } from 'react'; -import { useLogContent } from '@/hooks/use-log-content'; +import { useDialog } from '@/hooks/use-dialog'; -export default function InstallationLog({ service, children }: { service: Service; children?: ReactNode }) { - const [open, setOpen] = useState(false); - - const { content, isLoading, error } = useLogContent({ - serverId: service.server_id, - logId: service.log?.id ?? 0, - enabled: open && !!service.log, - }); +export default function InstallationLog({ service }: { service: Service }) { + const dialog = useDialog(); if (!service.log) { return null; } + const logId = service.log.id; + return ( - - - {children ? children : e.preventDefault()}>Installation Log} - - - - Installation Log - Installation log for {service.name} - - - <> - {isLoading && 'Loading...'} - {error &&
Error: {error}
} - {content && !error && content} - -
- - - - - -
-
+ dialog.logViewer.open({ serverId: service.server_id, logId, title: 'Installation Log' })}> + Installation Log + ); } diff --git a/resources/js/stores/dialog-store.ts b/resources/js/stores/dialog-store.ts new file mode 100644 index 000000000..422fedc5f --- /dev/null +++ b/resources/js/stores/dialog-store.ts @@ -0,0 +1,21 @@ +import { create } from 'zustand'; +import type { DialogRegistry, ConsumerProps } from '@/components/dialogs/registry'; + +export type ActiveDialog = { + [K in keyof DialogRegistry]: { key: K; props: ConsumerProps }; +}[keyof DialogRegistry]; + +type DialogStore = { + active: ActiveDialog | null; + open: (key: K, props: ConsumerProps) => void; + close: () => void; +}; + +export const useDialogStore = create((set) => ({ + active: null, + open: (key, props) => + // TS cannot narrow the mapped-type union from correlated generics — + // `key` and `props` come from the same call so the cast is safe. + set({ active: { key, props } as ActiveDialog }), + close: () => set({ active: null }), +})); From cbfc205a773259cf2e1bd5f94e0f68da4de25b15 Mon Sep 17 00:00:00 2001 From: Richard Anderson Date: Sun, 31 May 2026 17:14:37 +0100 Subject: [PATCH 2/8] refactor --- app/Tables/Servers/FirewallRuleTable.php | 4 +- .../dialogs/activate-server-ssl-dialog.tsx | 83 +++++++++ .../dialogs/confirmation-dialog.tsx | 86 +++++++++ .../js/components/dialogs/dialog-host.tsx | 10 +- .../components/dialogs/log-viewer-dialog.tsx | 2 +- .../dialogs/php-extensions-dialog.tsx | 90 +++++++++ .../js/components/dialogs/php-ini-dialog.tsx | 110 +++++++++++ .../components/dialogs/plugin-logs-dialog.tsx | 109 +++++++++++ resources/js/components/dialogs/registry.ts | 51 +++++ .../components/dialogs/worker-logs-dialog.tsx | 41 ++++ resources/js/components/server-banners.tsx | 45 +++-- resources/js/hooks/use-dialog.ts | 2 +- .../js/pages/api-keys/components/columns.tsx | 67 ++----- .../js/pages/backups/components/columns.tsx | 81 +++----- .../pages/backups/components/edit-backup.tsx | 25 +-- .../pages/backups/components/file-columns.tsx | 87 +++------ .../backups/components/restore-backup.tsx | 35 +--- .../js/pages/commands/components/columns.tsx | 77 +++----- .../commands/components/edit-command.tsx | 26 +-- .../js/pages/cronjobs/components/columns.tsx | 116 +++++------- .../js/pages/cronjobs/components/form.tsx | 41 +--- resources/js/pages/cronjobs/index.tsx | 13 +- .../database-users/components/columns.tsx | 141 +++----------- .../components/edit-database-user.tsx | 134 +++---------- .../database-users/components/link-dialog.tsx | 77 ++++++++ .../js/pages/databases/components/delete.tsx | 71 ++----- .../pages/dns-providers/components/delete.tsx | 71 ++----- .../dns-providers/components/edit-dialog.tsx | 94 ++++++++++ .../pages/dns-providers/components/edit.tsx | 96 +--------- .../js/pages/domains/components/columns.tsx | 73 ++------ .../domains/components/record-columns.tsx | 95 +++------- .../pages/domains/components/record-form.tsx | 48 +---- resources/js/pages/domains/show.tsx | 13 +- .../js/pages/firewall/components/delete.tsx | 71 ++----- .../js/pages/firewall/components/form.tsx | 31 +-- resources/js/pages/firewall/index.tsx | 22 +-- .../components/create-hosted-domain.tsx | 84 +++------ .../components/edit-hosted-domain.tsx | 50 ++--- resources/js/pages/hosted-domains/index.tsx | 169 ++++------------- .../pages/monitoring/components/actions.tsx | 153 +++------------ .../components/data-retention-dialog.tsx | 74 ++++++++ .../components/delete.tsx | 71 ++----- .../components/edit-dialog.tsx | 76 ++++++++ .../notification-channels/components/edit.tsx | 82 +------- .../js/pages/php/components/default-cli.tsx | 73 ++------ .../js/pages/php/components/extensions.tsx | 95 +--------- resources/js/pages/php/components/ini.tsx | 105 +---------- .../pages/plugins/components/delete-logs.tsx | 70 ++----- .../js/pages/plugins/components/disable.tsx | 73 ++------ .../js/pages/plugins/components/enable.tsx | 69 ++----- .../js/pages/plugins/components/install.tsx | 69 ++----- .../js/pages/plugins/components/installed.tsx | 9 +- .../js/pages/plugins/components/uninstall.tsx | 76 ++------ .../js/pages/plugins/components/update.tsx | 69 ++----- .../js/pages/plugins/components/view-logs.tsx | 117 +----------- .../pages/projects/components/invitations.tsx | 114 ++++-------- .../js/pages/redirects/components/columns.tsx | 74 ++------ .../js/pages/scripts/components/columns.tsx | 77 +++----- .../js/pages/scripts/components/form.tsx | 29 ++- resources/js/pages/scripts/index.tsx | 41 ++-- .../pages/server-logs/components/columns.tsx | 121 ++++-------- .../server-providers/components/delete.tsx | 71 ++----- .../components/edit-dialog.tsx | 76 ++++++++ .../server-providers/components/edit.tsx | 82 +------- .../server-ssh-keys/components/delete.tsx | 76 ++------ .../components/activate-server-ssl.tsx | 89 +-------- .../pages/server-ssls/components/columns.tsx | 75 ++------ .../js/pages/servers/components/actions.tsx | 41 +++- .../servers/components/reboot-server.tsx | 58 ------ .../servers/components/update-server.tsx | 62 ------ .../js/pages/services/components/action.tsx | 78 ++------ .../components/config-file-dialog.tsx | 106 +++++++++++ .../pages/services/components/config-file.tsx | 101 +--------- .../pages/services/components/uninstall.tsx | 69 ++----- .../source-controls/components/columns.tsx | 176 +++--------------- .../components/edit-dialog.tsx | 109 +++++++++++ .../js/pages/ssh-keys/components/delete.tsx | 65 ++----- .../storage-providers/components/delete.tsx | 71 ++----- .../components/edit-dialog.tsx | 76 ++++++++ .../storage-providers/components/edit.tsx | 82 +------- .../js/pages/workers/components/columns.tsx | 127 +++++-------- .../js/pages/workers/components/form.tsx | 52 ++---- .../workers/components/worker-row-actions.tsx | 103 ++-------- resources/js/pages/workers/index.tsx | 13 +- resources/js/stores/dialog-store.ts | 25 ++- 85 files changed, 2385 insertions(+), 3776 deletions(-) create mode 100644 resources/js/components/dialogs/activate-server-ssl-dialog.tsx create mode 100644 resources/js/components/dialogs/confirmation-dialog.tsx create mode 100644 resources/js/components/dialogs/php-extensions-dialog.tsx create mode 100644 resources/js/components/dialogs/php-ini-dialog.tsx create mode 100644 resources/js/components/dialogs/plugin-logs-dialog.tsx create mode 100644 resources/js/components/dialogs/worker-logs-dialog.tsx create mode 100644 resources/js/pages/database-users/components/link-dialog.tsx create mode 100644 resources/js/pages/dns-providers/components/edit-dialog.tsx create mode 100644 resources/js/pages/monitoring/components/data-retention-dialog.tsx create mode 100644 resources/js/pages/notification-channels/components/edit-dialog.tsx create mode 100644 resources/js/pages/server-providers/components/edit-dialog.tsx delete mode 100644 resources/js/pages/servers/components/reboot-server.tsx delete mode 100644 resources/js/pages/servers/components/update-server.tsx create mode 100644 resources/js/pages/services/components/config-file-dialog.tsx create mode 100644 resources/js/pages/source-controls/components/edit-dialog.tsx create mode 100644 resources/js/pages/storage-providers/components/edit-dialog.tsx diff --git a/app/Tables/Servers/FirewallRuleTable.php b/app/Tables/Servers/FirewallRuleTable.php index bbe6fb939..c01fb7ab9 100644 --- a/app/Tables/Servers/FirewallRuleTable.php +++ b/app/Tables/Servers/FirewallRuleTable.php @@ -22,9 +22,9 @@ protected function columns(): array { return [ TextColumn::make('name', 'Name')->sortable(), - TextColumn::make('type', 'Type')->uppercase()->sortable(), + TextColumn::make('type', 'Type')->sortable(), TextColumn::make('source', 'Source')->fallback('any')->sortable(), - TextColumn::make('protocol', 'Protocol')->uppercase()->sortable(), + TextColumn::make('protocol', 'Protocol')->sortable(), TextColumn::make('port', 'Port')->sortable(), EnumColumn::make('status', 'Status')->sortable(), Column::data('id'), diff --git a/resources/js/components/dialogs/activate-server-ssl-dialog.tsx b/resources/js/components/dialogs/activate-server-ssl-dialog.tsx new file mode 100644 index 000000000..b39836975 --- /dev/null +++ b/resources/js/components/dialogs/activate-server-ssl-dialog.tsx @@ -0,0 +1,83 @@ +import { FormEvent } from 'react'; +import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet'; +import { Form, FormField, FormFields } from '@/components/ui/form'; +import { useForm } from '@inertiajs/react'; +import { Button } from '@/components/ui/button'; +import { LoaderCircle } from 'lucide-react'; +import { Label } from '@/components/ui/label'; +import InputError from '@/components/ui/input-error'; +import { Textarea } from '@/components/ui/textarea'; + +type ActivateServerSslDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + serverId: number; + sslId: number; +}; + +export default function ActivateServerSslDialog({ open, onOpenChange, serverId, sslId }: ActivateServerSslDialogProps) { + const form = useForm<{ certificate: string; ca: string }>({ + certificate: '', + ca: '', + }); + + const submit = (e: FormEvent) => { + e.preventDefault(); + form.post(route('server-ssls.activate', { server: serverId, ssl: sslId }), { + onSuccess: () => onOpenChange(false), + }); + }; + + return ( + + e.preventDefault()}> + + Activate SSL + Install a signed certificate from your Certificate Authority. + +
+ + + +