Format the repository with Prettier
CI has been running `prettier --check` against a tree that was never formatted, so the check reported 63 files and failed every run. Nothing here is a behaviour change: `pnpm lint` and the four typecheck builds pass exactly as before. conduit-bringup-artifacts is added to .prettierignore instead. Those files are captured bring-up reports, not maintained sources; reflowing them would only churn a record of what happened.
This commit is contained in:
+49
-49
@@ -76,62 +76,62 @@ function AuthGuard() {
|
||||
export function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* Public routes */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* Public routes */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
|
||||
{/* Protected routes */}
|
||||
<Route element={<AuthGuard />}>
|
||||
<Route element={<AppLayout />}>
|
||||
{/* Organizations */}
|
||||
<Route path="/" element={<OrganizationsPage />} />
|
||||
{/* Protected routes */}
|
||||
<Route element={<AuthGuard />}>
|
||||
<Route element={<AppLayout />}>
|
||||
{/* Organizations */}
|
||||
<Route path="/" element={<OrganizationsPage />} />
|
||||
|
||||
{/* Org-scoped routes */}
|
||||
<Route path="/org/:orgId/dashboard" element={<DashboardPage />} />
|
||||
<Route path="/org/:orgId/servers" element={<ServersPage />} />
|
||||
<Route path="/org/:orgId/servers/new" element={<CreateServerPage />} />
|
||||
<Route path="/org/:orgId/nodes" element={<NodesPage />} />
|
||||
<Route path="/org/:orgId/nodes/:nodeId" element={<NodeDetailPage />} />
|
||||
<Route path="/org/:orgId/settings" element={<Navigate to="members" replace />} />
|
||||
<Route path="/org/:orgId/settings/members" element={<MembersPage />} />
|
||||
{/* Org-scoped routes */}
|
||||
<Route path="/org/:orgId/dashboard" element={<DashboardPage />} />
|
||||
<Route path="/org/:orgId/servers" element={<ServersPage />} />
|
||||
<Route path="/org/:orgId/servers/new" element={<CreateServerPage />} />
|
||||
<Route path="/org/:orgId/nodes" element={<NodesPage />} />
|
||||
<Route path="/org/:orgId/nodes/:nodeId" element={<NodeDetailPage />} />
|
||||
<Route path="/org/:orgId/settings" element={<Navigate to="members" replace />} />
|
||||
<Route path="/org/:orgId/settings/members" element={<MembersPage />} />
|
||||
|
||||
{/* Account */}
|
||||
<Route path="/account/security" element={<AccountSecurityPage />} />
|
||||
{/* Account */}
|
||||
<Route path="/account/security" element={<AccountSecurityPage />} />
|
||||
|
||||
{/* Server detail */}
|
||||
<Route path="/org/:orgId/servers/:serverId" element={<ServerLayout />}>
|
||||
<Route index element={<Navigate to="console" replace />} />
|
||||
<Route path="console" element={<ConsolePage />} />
|
||||
<Route path="files" element={<FilesPage />} />
|
||||
<Route path="config" element={<ConfigPage />} />
|
||||
<Route path="databases" element={<DatabasesPage />} />
|
||||
<Route path="plugins" element={<PluginsPage />} />
|
||||
<Route path="backups" element={<BackupsPage />} />
|
||||
<Route path="schedules" element={<SchedulesPage />} />
|
||||
<Route path="players" element={<PlayersPage />} />
|
||||
<Route path="settings" element={<ServerSettingsPage />} />
|
||||
{/* Server detail */}
|
||||
<Route path="/org/:orgId/servers/:serverId" element={<ServerLayout />}>
|
||||
<Route index element={<Navigate to="console" replace />} />
|
||||
<Route path="console" element={<ConsolePage />} />
|
||||
<Route path="files" element={<FilesPage />} />
|
||||
<Route path="config" element={<ConfigPage />} />
|
||||
<Route path="databases" element={<DatabasesPage />} />
|
||||
<Route path="plugins" element={<PluginsPage />} />
|
||||
<Route path="backups" element={<BackupsPage />} />
|
||||
<Route path="schedules" element={<SchedulesPage />} />
|
||||
<Route path="players" element={<PlayersPage />} />
|
||||
<Route path="settings" element={<ServerSettingsPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Admin */}
|
||||
<Route path="/admin/users" element={<AdminUsersPage />} />
|
||||
<Route path="/admin/games" element={<AdminGamesPage />} />
|
||||
<Route path="/admin/plugins" element={<AdminPluginsPage />} />
|
||||
<Route path="/admin/nodes" element={<AdminNodesPage />} />
|
||||
<Route path="/admin/audit-logs" element={<AdminAuditLogsPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Admin */}
|
||||
<Route path="/admin/users" element={<AdminUsersPage />} />
|
||||
<Route path="/admin/games" element={<AdminGamesPage />} />
|
||||
<Route path="/admin/plugins" element={<AdminPluginsPage />} />
|
||||
<Route path="/admin/nodes" element={<AdminNodesPage />} />
|
||||
<Route path="/admin/audit-logs" element={<AdminAuditLogsPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* Fallback */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<Toaster position="bottom-right" richColors />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
{/* Fallback */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<Toaster position="bottom-right" richColors />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Outlet, useParams, Link, useLocation } from 'react-router';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Terminal, FolderOpen, Settings, Calendar, HardDrive, Users, Puzzle, Settings2, Database as DatabaseIcon } from 'lucide-react';
|
||||
import {
|
||||
Terminal,
|
||||
FolderOpen,
|
||||
Settings,
|
||||
Calendar,
|
||||
HardDrive,
|
||||
Users,
|
||||
Puzzle,
|
||||
Settings2,
|
||||
Database as DatabaseIcon,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@source/ui';
|
||||
import { api } from '@/lib/api';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -52,9 +62,7 @@ export function ServerLayout() {
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold">{server?.name ?? 'Loading...'}</h1>
|
||||
{server && (
|
||||
<Badge variant={statusBadgeVariant(server.status)}>{server.status}</Badge>
|
||||
)}
|
||||
{server && <Badge variant={statusBadgeVariant(server.status)}>{server.status}</Badge>}
|
||||
</div>
|
||||
{server && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
|
||||
@@ -96,8 +96,7 @@ function NavSection({ items, currentPath }: { items: NavItem[]; currentPath: str
|
||||
return (
|
||||
<nav className="flex flex-col gap-1">
|
||||
{items.map((item) => {
|
||||
const isActive =
|
||||
currentPath === item.href || currentPath.startsWith(item.href + '/');
|
||||
const isActive = currentPath === item.href || currentPath.startsWith(item.href + '/');
|
||||
return (
|
||||
<Link key={item.href} to={item.href}>
|
||||
<Button
|
||||
|
||||
@@ -93,11 +93,7 @@ export function PowerControls({ serverId, orgId, status }: PowerControlsProps) {
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
disabled={isTransitioning && !isRunning}
|
||||
>
|
||||
<Button size="sm" variant="destructive" disabled={isTransitioning && !isRunning}>
|
||||
<Skull className="h-4 w-4" />
|
||||
Kill
|
||||
</Button>
|
||||
|
||||
@@ -18,8 +18,7 @@ const badgeVariants = cva(
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
|
||||
@@ -10,7 +10,8 @@ const buttonVariants = cva(
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
@@ -30,15 +31,16 @@ const buttonVariants = cva(
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
||||
return (
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
@@ -3,7 +3,11 @@ import { cn } from '@source/ui';
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('rounded-xl border bg-card text-card-foreground shadow', className)} {...props} />
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Card.displayName = 'Card';
|
||||
@@ -17,7 +21,11 @@ CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('font-semibold leading-none tracking-tight', className)} {...props} />
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
@@ -30,7 +38,9 @@ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HT
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />,
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
),
|
||||
);
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
|
||||
@@ -53,7 +53,10 @@ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
|
||||
);
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
@@ -72,7 +75,11 @@ const DialogDescription = React.forwardRef<
|
||||
React.ComponentRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
|
||||
@@ -44,7 +44,11 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ const ScrollArea = React.forwardRef<
|
||||
React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root ref={ref} className={cn('relative overflow-hidden', className)} {...props}>
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
|
||||
@@ -111,8 +111,7 @@ async function refreshToken(): Promise<boolean> {
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string, params?: Record<string, string>) =>
|
||||
request<T>(path, { params }),
|
||||
get: <T>(path: string, params?: Record<string, string>) => request<T>(path, { params }),
|
||||
|
||||
post: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, {
|
||||
@@ -132,8 +131,7 @@ export const api = {
|
||||
body: toRequestBody(body),
|
||||
}),
|
||||
|
||||
delete: <T>(path: string) =>
|
||||
request<T>(path, { method: 'DELETE' }),
|
||||
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
export { ApiError };
|
||||
|
||||
@@ -186,9 +186,7 @@ export function AdminGamesPage() {
|
||||
<Label>Slug</Label>
|
||||
<Input
|
||||
value={slug}
|
||||
onChange={(e) =>
|
||||
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))
|
||||
}
|
||||
onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -241,7 +239,10 @@ export function AdminGamesPage() {
|
||||
</p>
|
||||
<p className="mt-2 font-mono text-xs">{game.dockerImage}</p>
|
||||
<p>Port: {game.defaultPort}</p>
|
||||
<p>Automation: {Array.isArray(game.automationRules) ? game.automationRules.length : 0} workflow</p>
|
||||
<p>
|
||||
Automation:{' '}
|
||||
{Array.isArray(game.automationRules) ? game.automationRules.length : 0} workflow
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -277,7 +278,8 @@ export function AdminGamesPage() {
|
||||
<div className="space-y-2">
|
||||
<Label>JSON</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Supported events: server.created, server.install.completed, server.power.started, server.power.stopped
|
||||
Supported events: server.created, server.install.completed, server.power.started,
|
||||
server.power.stopped
|
||||
</p>
|
||||
<textarea
|
||||
value={automationJson}
|
||||
|
||||
@@ -42,14 +42,20 @@ export function AdminNodesPage() {
|
||||
</div>
|
||||
<Badge variant={node.isOnline ? 'default' : 'destructive'}>
|
||||
{node.isOnline ? (
|
||||
<><Wifi className="mr-1 h-3 w-3" /> Online</>
|
||||
<>
|
||||
<Wifi className="mr-1 h-3 w-3" /> Online
|
||||
</>
|
||||
) : (
|
||||
<><WifiOff className="mr-1 h-3 w-3" /> Offline</>
|
||||
<>
|
||||
<WifiOff className="mr-1 h-3 w-3" /> Offline
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">{node.fqdn}:{node.daemonPort}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{node.fqdn}:{node.daemonPort}
|
||||
</p>
|
||||
<div className="mt-3 flex gap-4 text-sm">
|
||||
<span>{formatBytes(node.memoryTotal)} RAM</span>
|
||||
<span>{formatBytes(node.diskTotal)} Disk</span>
|
||||
|
||||
@@ -197,12 +197,14 @@ export function AdminPluginsPage() {
|
||||
const map = new Map<string, File>();
|
||||
|
||||
for (const item of prev) {
|
||||
const relative = (item as File & { webkitRelativePath?: string }).webkitRelativePath || item.name;
|
||||
const relative =
|
||||
(item as File & { webkitRelativePath?: string }).webkitRelativePath || item.name;
|
||||
map.set(`${relative}::${item.size}::${item.lastModified}`, item);
|
||||
}
|
||||
|
||||
for (const item of Array.from(incoming)) {
|
||||
const relative = (item as File & { webkitRelativePath?: string }).webkitRelativePath || item.name;
|
||||
const relative =
|
||||
(item as File & { webkitRelativePath?: string }).webkitRelativePath || item.name;
|
||||
map.set(`${relative}::${item.size}::${item.lastModified}`, item);
|
||||
}
|
||||
|
||||
@@ -211,12 +213,8 @@ export function AdminPluginsPage() {
|
||||
};
|
||||
|
||||
const createPluginMutation = useMutation({
|
||||
mutationFn: (body: {
|
||||
gameId: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
}) => api.post('/admin/plugins', body),
|
||||
mutationFn: (body: { gameId: string; name: string; slug?: string; description?: string }) =>
|
||||
api.post('/admin/plugins', body),
|
||||
onSuccess: () => {
|
||||
toast.success('Global plugin created');
|
||||
setCreatePluginOpen(false);
|
||||
@@ -366,15 +364,25 @@ export function AdminPluginsPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Name</Label>
|
||||
<Input value={createPluginName} onChange={(e) => setCreatePluginName(e.target.value)} required />
|
||||
<Input
|
||||
value={createPluginName}
|
||||
onChange={(e) => setCreatePluginName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Slug (optional)</Label>
|
||||
<Input value={createPluginSlug} onChange={(e) => setCreatePluginSlug(e.target.value)} />
|
||||
<Input
|
||||
value={createPluginSlug}
|
||||
onChange={(e) => setCreatePluginSlug(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Description (optional)</Label>
|
||||
<Input value={createPluginDescription} onChange={(e) => setCreatePluginDescription(e.target.value)} />
|
||||
<Input
|
||||
value={createPluginDescription}
|
||||
onChange={(e) => setCreatePluginDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={createPluginMutation.isPending}>
|
||||
@@ -424,7 +432,9 @@ export function AdminPluginsPage() {
|
||||
type="button"
|
||||
onClick={() => setSelectedPluginId(plugin.id)}
|
||||
className={`w-full rounded-md border px-3 py-2 text-left transition ${
|
||||
selectedPluginId === plugin.id ? 'border-primary bg-primary/5' : 'hover:bg-muted/40'
|
||||
selectedPluginId === plugin.id
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'hover:bg-muted/40'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -454,11 +464,7 @@ export function AdminPluginsPage() {
|
||||
>
|
||||
<Copy className="h-4 w-4" /> Clone Latest
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => openReleaseDialogFrom()}
|
||||
disabled={!selectedPlugin}
|
||||
>
|
||||
<Button size="sm" onClick={() => openReleaseDialogFrom()} disabled={!selectedPlugin}>
|
||||
<UploadCloud className="h-4 w-4" /> New Release
|
||||
</Button>
|
||||
</div>
|
||||
@@ -478,9 +484,12 @@ export function AdminPluginsPage() {
|
||||
<Badge variant="secondary">{release.artifactType}</Badge>
|
||||
{!release.isPublished && <Badge variant="destructive">Unpublished</Badge>}
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-1 text-xs text-muted-foreground">{release.artifactUrl}</p>
|
||||
<p className="mt-1 line-clamp-1 text-xs text-muted-foreground">
|
||||
{release.artifactUrl}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Schema: {Array.isArray(release.installSchema) ? release.installSchema.length : 0} fields • Templates:{' '}
|
||||
Schema: {Array.isArray(release.installSchema) ? release.installSchema.length : 0}{' '}
|
||||
fields • Templates:{' '}
|
||||
{Array.isArray(release.configTemplates) ? release.configTemplates.length : 0}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
@@ -517,7 +526,9 @@ export function AdminPluginsPage() {
|
||||
>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Publish Release{selectedPlugin ? ` - ${selectedPlugin.name}` : ''}</DialogTitle>
|
||||
<DialogTitle>
|
||||
Publish Release{selectedPlugin ? ` - ${selectedPlugin.name}` : ''}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form
|
||||
className="space-y-4"
|
||||
@@ -540,7 +551,8 @@ export function AdminPluginsPage() {
|
||||
const formData = new FormData();
|
||||
formData.append('version', releaseVersion);
|
||||
formData.append('channel', releaseChannel);
|
||||
if (releaseDestination.trim()) formData.append('destination', releaseDestination.trim());
|
||||
if (releaseDestination.trim())
|
||||
formData.append('destination', releaseDestination.trim());
|
||||
if (releaseFileName.trim()) formData.append('fileName', releaseFileName.trim());
|
||||
if (releaseChangelog.trim()) formData.append('changelog', releaseChangelog);
|
||||
if (releaseInstallSchemaFile) {
|
||||
@@ -563,8 +575,12 @@ export function AdminPluginsPage() {
|
||||
}
|
||||
|
||||
for (const file of releaseArtifactFiles) {
|
||||
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath;
|
||||
formData.append('relativePath', relativePath && relativePath.length > 0 ? relativePath : file.name);
|
||||
const relativePath = (file as File & { webkitRelativePath?: string })
|
||||
.webkitRelativePath;
|
||||
formData.append(
|
||||
'relativePath',
|
||||
relativePath && relativePath.length > 0 ? relativePath : file.name,
|
||||
);
|
||||
formData.append('files', file, file.name);
|
||||
}
|
||||
|
||||
@@ -592,7 +608,11 @@ export function AdminPluginsPage() {
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Version</Label>
|
||||
<Input value={releaseVersion} onChange={(e) => setReleaseVersion(e.target.value)} required />
|
||||
<Input
|
||||
value={releaseVersion}
|
||||
onChange={(e) => setReleaseVersion(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Channel</Label>
|
||||
@@ -658,8 +678,8 @@ export function AdminPluginsPage() {
|
||||
{releaseInputMode === 'upload' && (
|
||||
<div className="space-y-3 rounded-md border p-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Tek dosya secersen tekil upload olur. Birden fazla dosya veya klasor secersen otomatik zip
|
||||
yapilip CDN'e yuklenir.
|
||||
Tek dosya secersen tekil upload olur. Birden fazla dosya veya klasor secersen
|
||||
otomatik zip yapilip CDN'e yuklenir.
|
||||
</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
@@ -683,9 +703,16 @@ export function AdminPluginsPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">Selected: {releaseArtifactFiles.length} file(s)</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Selected: {releaseArtifactFiles.length} file(s)
|
||||
</p>
|
||||
{releaseArtifactFiles.length > 0 && (
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => setReleaseArtifactFiles([])}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setReleaseArtifactFiles([])}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
@@ -693,7 +720,8 @@ export function AdminPluginsPage() {
|
||||
{releaseArtifactFiles.length > 0 && (
|
||||
<div className="max-h-28 space-y-1 overflow-auto rounded bg-muted/40 p-2 text-xs">
|
||||
{releaseArtifactFiles.map((file, index) => {
|
||||
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath;
|
||||
const relativePath = (file as File & { webkitRelativePath?: string })
|
||||
.webkitRelativePath;
|
||||
return (
|
||||
<p key={`${relativePath || file.name}-${index}`} className="truncate">
|
||||
{relativePath || file.name}
|
||||
@@ -745,7 +773,10 @@ export function AdminPluginsPage() {
|
||||
/>
|
||||
{releaseInstallSchemaFile && (
|
||||
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||
<p>File secili: {releaseInstallSchemaFile.name}. Bu dosya, alttaki metni override eder.</p>
|
||||
<p>
|
||||
File secili: {releaseInstallSchemaFile.name}. Bu dosya, alttaki metni override
|
||||
eder.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -776,7 +807,10 @@ export function AdminPluginsPage() {
|
||||
/>
|
||||
{releaseTemplatesFile && (
|
||||
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||
<p>File secili: {releaseTemplatesFile.name}. Bu dosya, alttaki metni override eder.</p>
|
||||
<p>
|
||||
File secili: {releaseTemplatesFile.name}. Bu dosya, alttaki metni override
|
||||
eder.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -807,7 +841,7 @@ export function AdminPluginsPage() {
|
||||
!selectedPlugin
|
||||
}
|
||||
>
|
||||
{(createReleaseMutation.isPending || createUploadReleaseMutation.isPending)
|
||||
{createReleaseMutation.isPending || createUploadReleaseMutation.isPending
|
||||
? 'Publishing...'
|
||||
: 'Publish Release'}
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,14 @@ import { Shield } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { ApiError } from '@/lib/api';
|
||||
|
||||
@@ -47,7 +54,9 @@ export function LoginPage() {
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-4">
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{error}</div>
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
|
||||
@@ -4,7 +4,14 @@ import { Shield } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { ApiError } from '@/lib/api';
|
||||
|
||||
@@ -48,7 +55,9 @@ export function RegisterPage() {
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-4">
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{error}</div>
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
|
||||
@@ -54,7 +54,9 @@ export function DashboardPage() {
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total Servers</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Total Servers
|
||||
</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -93,17 +93,13 @@ export function NodeDetailPage() {
|
||||
const { data: serversData } = useQuery({
|
||||
queryKey: ['node-servers', orgId, nodeId],
|
||||
queryFn: () =>
|
||||
api.get<{ data: ServerSummary[] }>(
|
||||
`/organizations/${orgId}/nodes/${nodeId}/servers`,
|
||||
),
|
||||
api.get<{ data: ServerSummary[] }>(`/organizations/${orgId}/nodes/${nodeId}/servers`),
|
||||
});
|
||||
|
||||
const { data: allocData } = useQuery({
|
||||
queryKey: ['allocations', orgId, nodeId],
|
||||
queryFn: () =>
|
||||
api.get<{ data: Allocation[] }>(
|
||||
`/organizations/${orgId}/nodes/${nodeId}/allocations`,
|
||||
),
|
||||
api.get<{ data: Allocation[] }>(`/organizations/${orgId}/nodes/${nodeId}/allocations`),
|
||||
});
|
||||
|
||||
const allocations = allocData?.data ?? [];
|
||||
@@ -142,12 +138,10 @@ export function NodeDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const memPercent = stats && stats.memoryTotal > 0
|
||||
? Math.round((stats.memoryUsed / stats.memoryTotal) * 100)
|
||||
: 0;
|
||||
const diskPercent = stats && stats.diskTotal > 0
|
||||
? Math.round((stats.diskUsed / stats.diskTotal) * 100)
|
||||
: 0;
|
||||
const memPercent =
|
||||
stats && stats.memoryTotal > 0 ? Math.round((stats.memoryUsed / stats.memoryTotal) * 100) : 0;
|
||||
const diskPercent =
|
||||
stats && stats.diskTotal > 0 ? Math.round((stats.diskUsed / stats.diskTotal) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -167,9 +161,13 @@ export function NodeDetailPage() {
|
||||
</div>
|
||||
<Badge variant={node.isOnline ? 'default' : 'destructive'}>
|
||||
{node.isOnline ? (
|
||||
<><Wifi className="mr-1 h-3 w-3" /> Online</>
|
||||
<>
|
||||
<Wifi className="mr-1 h-3 w-3" /> Online
|
||||
</>
|
||||
) : (
|
||||
<><WifiOff className="mr-1 h-3 w-3" /> Offline</>
|
||||
<>
|
||||
<WifiOff className="mr-1 h-3 w-3" /> Offline
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -197,9 +195,7 @@ export function NodeDetailPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{stats
|
||||
? `${formatBytes(stats.memoryUsed)} / ${formatBytes(stats.memoryTotal)}`
|
||||
: '—'}
|
||||
{stats ? `${formatBytes(stats.memoryUsed)} / ${formatBytes(stats.memoryTotal)}` : '—'}
|
||||
</div>
|
||||
<Progress value={memPercent} className="mt-2 h-2" />
|
||||
</CardContent>
|
||||
@@ -212,9 +208,7 @@ export function NodeDetailPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{stats
|
||||
? `${formatBytes(stats.diskUsed)} / ${formatBytes(stats.diskTotal)}`
|
||||
: '—'}
|
||||
{stats ? `${formatBytes(stats.diskUsed)} / ${formatBytes(stats.diskTotal)}` : '—'}
|
||||
</div>
|
||||
<Progress value={diskPercent} className="mt-2 h-2" />
|
||||
</CardContent>
|
||||
@@ -248,9 +242,7 @@ export function NodeDetailPage() {
|
||||
<InfoRow label="gRPC Port" value={String(node.grpcPort)} />
|
||||
<InfoRow label="Total Memory" value={formatBytes(node.memoryTotal)} />
|
||||
<InfoRow label="Total Disk" value={formatBytes(node.diskTotal)} />
|
||||
{node.daemonVersion && (
|
||||
<InfoRow label="Daemon Version" value={node.daemonVersion} />
|
||||
)}
|
||||
{node.daemonVersion && <InfoRow label="Daemon Version" value={node.daemonVersion} />}
|
||||
<InfoRow label="Created" value={new Date(node.createdAt).toLocaleDateString()} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -277,9 +269,7 @@ export function NodeDetailPage() {
|
||||
<p className="text-xs text-muted-foreground">{srv.gameName}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={srv.status === 'running' ? 'default' : 'outline'}
|
||||
>
|
||||
<Badge variant={srv.status === 'running' ? 'default' : 'outline'}>
|
||||
{srv.status}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
@@ -373,7 +363,10 @@ export function NodeDetailPage() {
|
||||
/** Parse port input like "25565, 25566-25570, 27015" into flat number array */
|
||||
function parsePorts(input: string): number[] {
|
||||
const ports: number[] = [];
|
||||
const parts = input.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
const parts = input
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
for (const part of parts) {
|
||||
if (part.includes('-')) {
|
||||
const [startStr, endStr] = part.split('-');
|
||||
|
||||
@@ -183,16 +183,8 @@ export function NodesPage() {
|
||||
<div className="space-y-3">
|
||||
<Label>Daemon Token</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
readOnly
|
||||
value={createdToken}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleCopyToken}
|
||||
>
|
||||
<Input readOnly value={createdToken} className="font-mono text-xs" />
|
||||
<Button variant="outline" size="icon" onClick={handleCopyToken}>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
@@ -201,7 +193,8 @@ export function NodesPage() {
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Use this token in your daemon configuration file (config.yml) to authenticate with the panel.
|
||||
Use this token in your daemon configuration file (config.yml) to authenticate with the
|
||||
panel.
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
@@ -213,28 +206,34 @@ export function NodesPage() {
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{nodes.map((node) => (
|
||||
<Link key={node.id} to={`/org/${orgId}/nodes/${node.id}`}>
|
||||
<Card className="transition-colors hover:bg-muted/50 cursor-pointer">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Network className="h-5 w-5 text-primary" />
|
||||
<CardTitle className="text-base">{node.name}</CardTitle>
|
||||
</div>
|
||||
<Badge variant={node.isOnline ? 'default' : 'destructive'}>
|
||||
{node.isOnline ? (
|
||||
<><Wifi className="mr-1 h-3 w-3" /> Online</>
|
||||
) : (
|
||||
<><WifiOff className="mr-1 h-3 w-3" /> Offline</>
|
||||
)}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">{node.fqdn}:{node.daemonPort}</p>
|
||||
<div className="mt-3 flex gap-4 text-sm">
|
||||
<span>{formatBytes(node.memoryTotal)} RAM</span>
|
||||
<span>{formatBytes(node.diskTotal)} Disk</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="transition-colors hover:bg-muted/50 cursor-pointer">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Network className="h-5 w-5 text-primary" />
|
||||
<CardTitle className="text-base">{node.name}</CardTitle>
|
||||
</div>
|
||||
<Badge variant={node.isOnline ? 'default' : 'destructive'}>
|
||||
{node.isOnline ? (
|
||||
<>
|
||||
<Wifi className="mr-1 h-3 w-3" /> Online
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WifiOff className="mr-1 h-3 w-3" /> Offline
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{node.fqdn}:{node.daemonPort}
|
||||
</p>
|
||||
<div className="mt-3 flex gap-4 text-sm">
|
||||
<span>{formatBytes(node.memoryTotal)} RAM</span>
|
||||
<span>{formatBytes(node.diskTotal)} Disk</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -54,16 +54,13 @@ export function BackupsPage() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['backups', orgId, serverId],
|
||||
queryFn: () =>
|
||||
api.get<{ backups: Backup[] }>(
|
||||
`/organizations/${orgId}/servers/${serverId}/backups`,
|
||||
),
|
||||
api.get<{ backups: Backup[] }>(`/organizations/${orgId}/servers/${serverId}/backups`),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (backupId: string) =>
|
||||
api.delete(`/organizations/${orgId}/servers/${serverId}/backups/${backupId}`),
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: ['backups', orgId, serverId] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['backups', orgId, serverId] }),
|
||||
});
|
||||
|
||||
const restoreMutation = useMutation({
|
||||
@@ -75,8 +72,7 @@ export function BackupsPage() {
|
||||
const lockMutation = useMutation({
|
||||
mutationFn: (backupId: string) =>
|
||||
api.patch(`/organizations/${orgId}/servers/${serverId}/backups/${backupId}/lock`, {}),
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: ['backups', orgId, serverId] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['backups', orgId, serverId] }),
|
||||
});
|
||||
|
||||
const backupList = data?.backups ?? [];
|
||||
@@ -89,7 +85,8 @@ export function BackupsPage() {
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Backups</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{backupList.length} backup{backupList.length !== 1 ? 's' : ''} — {formatBytes(totalSize)} total
|
||||
{backupList.length} backup{backupList.length !== 1 ? 's' : ''} —{' '}
|
||||
{formatBytes(totalSize)} total
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={showCreate} onOpenChange={setShowCreate}>
|
||||
@@ -154,9 +151,7 @@ export function BackupsPage() {
|
||||
<span>{formatBytes(backup.sizeBytes)}</span>
|
||||
<span>{new Date(backup.createdAt).toLocaleString()}</span>
|
||||
{backup.checksum && (
|
||||
<span className="font-mono">
|
||||
{backup.checksum.slice(0, 12)}...
|
||||
</span>
|
||||
<span className="font-mono">{backup.checksum.slice(0, 12)}...</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,9 +233,7 @@ function CreateBackupForm({
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [name, setName] = useState(
|
||||
`backup-${new Date().toISOString().slice(0, 10)}`,
|
||||
);
|
||||
const [name, setName] = useState(`backup-${new Date().toISOString().slice(0, 10)}`);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: { name: string }) =>
|
||||
@@ -261,11 +254,7 @@ function CreateBackupForm({
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Backup Name</Label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} required />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
|
||||
@@ -30,10 +30,7 @@ interface ConfigDetail {
|
||||
raw: string;
|
||||
}
|
||||
|
||||
function mergeConfigEntries(
|
||||
entries: ConfigEntry[],
|
||||
editableKeys: string[] | null,
|
||||
): ConfigEntry[] {
|
||||
function mergeConfigEntries(entries: ConfigEntry[], editableKeys: string[] | null): ConfigEntry[] {
|
||||
if (!editableKeys || editableKeys.length === 0) return entries;
|
||||
|
||||
const existing = new Map(entries.map((entry) => [entry.key, entry]));
|
||||
@@ -55,9 +52,7 @@ export function ConfigPage() {
|
||||
const { data: configsData } = useQuery({
|
||||
queryKey: ['configs', orgId, serverId],
|
||||
queryFn: () =>
|
||||
api.get<{ configs: ConfigFile[] }>(
|
||||
`/organizations/${orgId}/servers/${serverId}/config`,
|
||||
),
|
||||
api.get<{ configs: ConfigFile[] }>(`/organizations/${orgId}/servers/${serverId}/config`),
|
||||
});
|
||||
|
||||
const configs = configsData?.configs ?? [];
|
||||
@@ -114,9 +109,7 @@ function ConfigEditor({
|
||||
const { data: detail } = useQuery({
|
||||
queryKey: ['config-detail', orgId, serverId, configIndex],
|
||||
queryFn: () =>
|
||||
api.get<ConfigDetail>(
|
||||
`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`,
|
||||
),
|
||||
api.get<ConfigDetail>(`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`),
|
||||
});
|
||||
|
||||
const [entries, setEntries] = useState<ConfigEntry[]>([]);
|
||||
@@ -128,10 +121,7 @@ function ConfigEditor({
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (data: { entries: ConfigEntry[] }) =>
|
||||
api.put(
|
||||
`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`,
|
||||
data,
|
||||
),
|
||||
api.put(`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['config-detail', orgId, serverId, configIndex],
|
||||
@@ -140,9 +130,7 @@ function ConfigEditor({
|
||||
});
|
||||
|
||||
const updateEntry = (key: string, value: string) => {
|
||||
setEntries((prev) =>
|
||||
prev.map((e) => (e.key === key ? { ...e, value } : e)),
|
||||
);
|
||||
setEntries((prev) => prev.map((e) => (e.key === key ? { ...e, value } : e)));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -171,15 +159,15 @@ function ConfigEditor({
|
||||
<CardContent>
|
||||
{entries.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||
{detail ? 'No entries found. The server may need to be started first to generate config files.' : 'Loading...'}
|
||||
{detail
|
||||
? 'No entries found. The server may need to be started first to generate config files.'
|
||||
: 'Loading...'}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{entries.map((entry) => (
|
||||
<div key={entry.key} className="grid gap-1.5">
|
||||
<Label className="font-mono text-xs text-muted-foreground">
|
||||
{entry.key}
|
||||
</Label>
|
||||
<Label className="font-mono text-xs text-muted-foreground">{entry.key}</Label>
|
||||
<Input
|
||||
value={entry.value}
|
||||
onChange={(e) => updateEntry(entry.key, e.target.value)}
|
||||
|
||||
@@ -6,7 +6,14 @@ import { toast } from 'sonner';
|
||||
import { ApiError, api } from '@/lib/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
@@ -38,9 +45,7 @@ function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs uppercase tracking-wide text-muted-foreground">{label}</p>
|
||||
<div className="rounded-md border bg-muted/40 px-3 py-2 font-mono text-xs">
|
||||
{value}
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 px-3 py-2 font-mono text-xs">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -59,9 +64,7 @@ export function DatabasesPage() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['server-databases', orgId, serverId],
|
||||
queryFn: () =>
|
||||
api.get<{ data: ManagedDatabase[] }>(
|
||||
`/organizations/${orgId}/servers/${serverId}/databases`,
|
||||
),
|
||||
api.get<{ data: ManagedDatabase[] }>(`/organizations/${orgId}/servers/${serverId}/databases`),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -270,11 +273,7 @@ export function DatabasesPage() {
|
||||
</a>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setEditingDatabase(database)}
|
||||
>
|
||||
<Button size="sm" variant="outline" onClick={() => setEditingDatabase(database)}>
|
||||
<RefreshCw className="h-4 w-4" /> Edit
|
||||
</Button>
|
||||
<Button
|
||||
@@ -307,7 +306,8 @@ export function DatabasesPage() {
|
||||
/>
|
||||
{!database.phpMyAdminUrl ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
phpMyAdmin link is not configured. Set `managed_mysql.phpmyadmin_url` in the daemon config for this node.
|
||||
phpMyAdmin link is not configured. Set `managed_mysql.phpmyadmin_url` in the
|
||||
daemon config for this node.
|
||||
</p>
|
||||
) : null}
|
||||
</CardContent>
|
||||
|
||||
@@ -68,10 +68,7 @@ function joinRemotePath(basePath: string, relativePath: string): string {
|
||||
.split('/')
|
||||
.filter((segment) => segment && segment !== '.' && segment !== '..');
|
||||
|
||||
const baseSegments = basePath
|
||||
.replace(/\\/g, '/')
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
const baseSegments = basePath.replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
|
||||
return `/${[...baseSegments, ...safeSegments].join('/')}`.replace(/\/{2,}/g, '/');
|
||||
}
|
||||
@@ -133,18 +130,16 @@ export function FilesPage() {
|
||||
null,
|
||||
);
|
||||
|
||||
const hasUnsavedChanges =
|
||||
!!editingFile && editingFile.content !== editingFile.originalContent;
|
||||
const hasUnsavedChanges = !!editingFile && editingFile.content !== editingFile.originalContent;
|
||||
const isUploading = !!uploadProgress;
|
||||
|
||||
const filesQuery = useQuery({
|
||||
queryKey: ['files', orgId, serverId, currentPath],
|
||||
enabled: Boolean(orgId && serverId) && !editingFile,
|
||||
queryFn: () =>
|
||||
api.get<{ files: FileEntry[] }>(
|
||||
`/organizations/${orgId}/servers/${serverId}/files`,
|
||||
{ path: currentPath },
|
||||
),
|
||||
api.get<{ files: FileEntry[] }>(`/organizations/${orgId}/servers/${serverId}/files`, {
|
||||
path: currentPath,
|
||||
}),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
@@ -528,9 +523,7 @@ export function FilesPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{hasUnsavedChanges && (
|
||||
<span className="text-xs text-amber-600">Unsaved changes</span>
|
||||
)}
|
||||
{hasUnsavedChanges && <span className="text-xs text-amber-600">Unsaved changes</span>}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={saveCurrentFile}
|
||||
@@ -550,9 +543,7 @@ export function FilesPage() {
|
||||
ref={editorRef}
|
||||
value={editingFile.content}
|
||||
onChange={(event) =>
|
||||
setEditingFile((prev) =>
|
||||
prev ? { ...prev, content: event.target.value } : prev,
|
||||
)
|
||||
setEditingFile((prev) => (prev ? { ...prev, content: event.target.value } : prev))
|
||||
}
|
||||
onKeyDown={handleEditorKeyDown}
|
||||
className="min-h-[560px] w-full resize-y rounded-md border bg-background p-3 font-mono text-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
@@ -611,21 +602,11 @@ export function FilesPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={triggerUploadFiles}
|
||||
disabled={isUploading}
|
||||
>
|
||||
<Button size="sm" variant="outline" onClick={triggerUploadFiles} disabled={isUploading}>
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload Files
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={triggerUploadFolder}
|
||||
disabled={isUploading}
|
||||
>
|
||||
<Button size="sm" variant="outline" onClick={triggerUploadFolder} disabled={isUploading}>
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload Folder
|
||||
</Button>
|
||||
@@ -746,11 +727,7 @@ export function FilesPage() {
|
||||
{filesQuery.isError && (
|
||||
<div className="space-y-2 py-8 text-center">
|
||||
<p className="text-sm text-destructive">Failed to load directory</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => filesQuery.refetch()}
|
||||
>
|
||||
<Button size="sm" variant="outline" onClick={() => filesQuery.refetch()}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
@@ -826,8 +803,7 @@ export function FilesPage() {
|
||||
<DialogTitle>{deleteTarget?.isDirectory ? 'Delete Folder' : 'Delete File'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Are you sure you want to delete{' '}
|
||||
<code className="font-mono">{deleteTarget?.path}</code>?
|
||||
Are you sure you want to delete <code className="font-mono">{deleteTarget?.path}</code>?
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
|
||||
@@ -21,9 +21,7 @@ export function PlayersPage() {
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey: ['players', orgId, serverId],
|
||||
queryFn: () =>
|
||||
api.get<PlayerListResponse>(
|
||||
`/organizations/${orgId}/servers/${serverId}/players`,
|
||||
),
|
||||
api.get<PlayerListResponse>(`/organizations/${orgId}/servers/${serverId}/players`),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
|
||||
@@ -293,7 +293,9 @@ function MarketplacePlugins({ orgId, serverId }: { orgId: string; serverId: stri
|
||||
const [installTarget, setInstallTarget] = useState<MarketplacePlugin | null>(null);
|
||||
const [installOptions, setInstallOptions] = useState<Record<string, unknown>>({});
|
||||
const [installPinVersion, setInstallPinVersion] = useState(false);
|
||||
const [installAutoUpdateChannel, setInstallAutoUpdateChannel] = useState<'stable' | 'beta' | 'alpha'>('stable');
|
||||
const [installAutoUpdateChannel, setInstallAutoUpdateChannel] = useState<
|
||||
'stable' | 'beta' | 'alpha'
|
||||
>('stable');
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['plugin-marketplace', orgId, serverId, searchTerm],
|
||||
@@ -317,7 +319,10 @@ function MarketplacePlugins({ orgId, serverId }: { orgId: string; serverId: stri
|
||||
autoUpdateChannel?: 'stable' | 'beta' | 'alpha';
|
||||
};
|
||||
}) =>
|
||||
api.post(`/organizations/${orgId}/servers/${serverId}/plugins/install/${pluginId}`, payload ?? {}),
|
||||
api.post(
|
||||
`/organizations/${orgId}/servers/${serverId}/plugins/install/${pluginId}`,
|
||||
payload ?? {},
|
||||
),
|
||||
onSuccess: () => {
|
||||
toast.success('Plugin installed');
|
||||
setInstallDialogOpen(false);
|
||||
@@ -364,8 +369,7 @@ function MarketplacePlugins({ orgId, serverId }: { orgId: string; serverId: stri
|
||||
description?: string;
|
||||
downloadUrl: string;
|
||||
version?: string;
|
||||
}) =>
|
||||
api.post(`/organizations/${orgId}/servers/${serverId}/plugins/marketplace`, body),
|
||||
}) => api.post(`/organizations/${orgId}/servers/${serverId}/plugins/marketplace`, body),
|
||||
onSuccess: () => {
|
||||
toast.success('Marketplace plugin added');
|
||||
setCreateOpen(false);
|
||||
@@ -655,7 +659,9 @@ function MarketplacePlugins({ orgId, serverId }: { orgId: string; serverId: stri
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => updateInstallMutation.mutate({ installId: plugin.installId! })}
|
||||
onClick={() =>
|
||||
updateInstallMutation.mutate({ installId: plugin.installId! })
|
||||
}
|
||||
disabled={updateInstallMutation.isPending}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
@@ -893,14 +899,18 @@ function InstalledPlugins({
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="font-medium">{plugin.name}</p>
|
||||
<Badge variant="outline">{plugin.source}</Badge>
|
||||
{plugin.installedVersion && <Badge variant="secondary">v{plugin.installedVersion}</Badge>}
|
||||
{plugin.installedVersion && (
|
||||
<Badge variant="secondary">v{plugin.installedVersion}</Badge>
|
||||
)}
|
||||
{!plugin.isActive && <Badge variant="secondary">Disabled</Badge>}
|
||||
{plugin.status !== 'installed' && (
|
||||
<Badge variant={plugin.status === 'failed' ? 'destructive' : 'outline'}>
|
||||
{plugin.status}
|
||||
</Badge>
|
||||
)}
|
||||
{plugin.updateAvailable && <Badge variant="destructive">Update Available</Badge>}
|
||||
{plugin.updateAvailable && (
|
||||
<Badge variant="destructive">Update Available</Badge>
|
||||
)}
|
||||
</div>
|
||||
{plugin.description && (
|
||||
<p className="text-sm text-muted-foreground">{plugin.description}</p>
|
||||
@@ -1157,7 +1167,8 @@ function ManualInstall({ orgId, serverId }: { orgId: string; serverId: string })
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Files sekmesinden dosyayı önce sunucuya yükleyin. Relative path girerseniz oyunun varsayılan plugin dizinine göre çözülür.
|
||||
Files sekmesinden dosyayı önce sunucuya yükleyin. Relative path girerseniz oyunun
|
||||
varsayılan plugin dizinine göre çözülür.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -63,30 +63,25 @@ export function SchedulesPage() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['schedules', orgId, serverId],
|
||||
queryFn: () =>
|
||||
api.get<{ tasks: ScheduledTask[] }>(
|
||||
`/organizations/${orgId}/servers/${serverId}/schedules`,
|
||||
),
|
||||
api.get<{ tasks: ScheduledTask[] }>(`/organizations/${orgId}/servers/${serverId}/schedules`),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (taskId: string) =>
|
||||
api.delete(`/organizations/${orgId}/servers/${serverId}/schedules/${taskId}`),
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
|
||||
});
|
||||
|
||||
const triggerMutation = useMutation({
|
||||
mutationFn: (taskId: string) =>
|
||||
api.post(`/organizations/${orgId}/servers/${serverId}/schedules/${taskId}/trigger`, {}),
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
|
||||
});
|
||||
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: ({ taskId, isActive }: { taskId: string; isActive: boolean }) =>
|
||||
api.patch(`/organizations/${orgId}/servers/${serverId}/schedules/${taskId}`, { isActive }),
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
|
||||
});
|
||||
|
||||
const tasks = data?.tasks ?? [];
|
||||
@@ -150,14 +145,10 @@ export function SchedulesPage() {
|
||||
{formatSchedule(task.scheduleType, task.scheduleData)}
|
||||
</span>
|
||||
{task.nextRunAt && (
|
||||
<span>
|
||||
Next: {new Date(task.nextRunAt).toLocaleString()}
|
||||
</span>
|
||||
<span>Next: {new Date(task.nextRunAt).toLocaleString()}</span>
|
||||
)}
|
||||
{task.lastRunAt && (
|
||||
<span>
|
||||
Last: {new Date(task.lastRunAt).toLocaleString()}
|
||||
</span>
|
||||
<span>Last: {new Date(task.lastRunAt).toLocaleString()}</span>
|
||||
)}
|
||||
</div>
|
||||
{task.action === 'command' && (
|
||||
@@ -188,11 +179,7 @@ export function SchedulesPage() {
|
||||
}
|
||||
title={task.isActive ? 'Pause' : 'Resume'}
|
||||
>
|
||||
{task.isActive ? (
|
||||
<Pause className="h-4 w-4" />
|
||||
) : (
|
||||
<Play className="h-4 w-4" />
|
||||
)}
|
||||
{task.isActive ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -243,7 +230,9 @@ function CreateScheduleForm({
|
||||
const [name, setName] = useState('');
|
||||
const [action, setAction] = useState<'command' | 'power' | 'backup'>('command');
|
||||
const [payload, setPayload] = useState('');
|
||||
const [scheduleType, setScheduleType] = useState<'interval' | 'daily' | 'weekly' | 'cron'>('interval');
|
||||
const [scheduleType, setScheduleType] = useState<'interval' | 'daily' | 'weekly' | 'cron'>(
|
||||
'interval',
|
||||
);
|
||||
|
||||
// Schedule data fields
|
||||
const [minutes, setMinutes] = useState('60');
|
||||
@@ -268,7 +257,11 @@ function CreateScheduleForm({
|
||||
case 'daily':
|
||||
return { hour: parseInt(hour, 10), minute: parseInt(minute, 10) };
|
||||
case 'weekly':
|
||||
return { dayOfWeek: parseInt(dayOfWeek, 10), hour: parseInt(hour, 10), minute: parseInt(minute, 10) };
|
||||
return {
|
||||
dayOfWeek: parseInt(dayOfWeek, 10),
|
||||
hour: parseInt(hour, 10),
|
||||
minute: parseInt(minute, 10),
|
||||
};
|
||||
case 'cron':
|
||||
return { expression: cronExpression };
|
||||
}
|
||||
@@ -301,7 +294,9 @@ function CreateScheduleForm({
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Action</Label>
|
||||
<Select value={action} onValueChange={(v) => setAction(v as typeof action)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="command">Run Command</SelectItem>
|
||||
<SelectItem value="power">Power Action</SelectItem>
|
||||
@@ -322,7 +317,9 @@ function CreateScheduleForm({
|
||||
/>
|
||||
) : (
|
||||
<Select value={payload} onValueChange={setPayload}>
|
||||
<SelectTrigger><SelectValue placeholder="Select..." /></SelectTrigger>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="start">Start</SelectItem>
|
||||
<SelectItem value="stop">Stop</SelectItem>
|
||||
@@ -337,8 +334,13 @@ function CreateScheduleForm({
|
||||
|
||||
<div className="grid gap-1.5">
|
||||
<Label>Schedule Type</Label>
|
||||
<Select value={scheduleType} onValueChange={(v) => setScheduleType(v as typeof scheduleType)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<Select
|
||||
value={scheduleType}
|
||||
onValueChange={(v) => setScheduleType(v as typeof scheduleType)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="interval">Interval</SelectItem>
|
||||
<SelectItem value="daily">Daily</SelectItem>
|
||||
@@ -366,10 +368,14 @@ function CreateScheduleForm({
|
||||
<div className="col-span-2 grid gap-1.5">
|
||||
<Label>Day of Week</Label>
|
||||
<Select value={dayOfWeek} onValueChange={setDayOfWeek}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DAYS_OF_WEEK.map((day, i) => (
|
||||
<SelectItem key={day} value={String(i)}>{day}</SelectItem>
|
||||
<SelectItem key={day} value={String(i)}>
|
||||
{day}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -8,7 +8,13 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { formatBytes } from '@/lib/utils';
|
||||
|
||||
interface ServerDetail {
|
||||
@@ -244,9 +250,13 @@ export function ServerSettingsPage() {
|
||||
const [description, setDescription] = useState('');
|
||||
const [startupOverride, setStartupOverride] = useState('');
|
||||
const [environmentFields, setEnvironmentFields] = useState<EnvironmentField[]>([]);
|
||||
const [automationEvent, setAutomationEvent] = useState<AutomationEvent>('server.install.completed');
|
||||
const [automationEvent, setAutomationEvent] = useState<AutomationEvent>(
|
||||
'server.install.completed',
|
||||
);
|
||||
const [forceAutomationRun, setForceAutomationRun] = useState(false);
|
||||
const [lastAutomationResult, setLastAutomationResult] = useState<AutomationRunResult | null>(null);
|
||||
const [lastAutomationResult, setLastAutomationResult] = useState<AutomationRunResult | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const { data: gamesData } = useQuery({
|
||||
queryKey: ['games'],
|
||||
@@ -290,7 +300,10 @@ export function ServerSettingsPage() {
|
||||
|
||||
const automationRunMutation = useMutation({
|
||||
mutationFn: (body: { event: AutomationEvent; force: boolean }) =>
|
||||
api.post<AutomationRunResponse>(`/organizations/${orgId}/servers/${serverId}/automation/run`, body),
|
||||
api.post<AutomationRunResponse>(
|
||||
`/organizations/${orgId}/servers/${serverId}/automation/run`,
|
||||
body,
|
||||
),
|
||||
onSuccess: (response) => {
|
||||
setLastAutomationResult(response.result);
|
||||
if (response.result.workflowsFailed > 0 || response.result.actionFailures > 0) {
|
||||
@@ -442,7 +455,7 @@ export function ServerSettingsPage() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{environmentFields.map((field, index) => (
|
||||
{environmentFields.map((field, index) =>
|
||||
field.isCustom ? (
|
||||
<div
|
||||
key={`custom-${index}`}
|
||||
@@ -477,7 +490,8 @@ export function ServerSettingsPage() {
|
||||
{field.label}
|
||||
</Label>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
Default: <span className="font-mono">{field.defaultValue || 'empty'}</span>
|
||||
Default:{' '}
|
||||
<span className="font-mono">{field.defaultValue || 'empty'}</span>
|
||||
</span>
|
||||
</div>
|
||||
{field.inputType === 'boolean' ? (
|
||||
@@ -513,16 +527,13 @@ export function ServerSettingsPage() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={saveStartupSettings}
|
||||
disabled={updateMutation.isPending || !server}
|
||||
>
|
||||
<Button onClick={saveStartupSettings} disabled={updateMutation.isPending || !server}>
|
||||
{updateMutation.isPending ? 'Applying...' : 'Save Startup Settings'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
@@ -536,7 +547,10 @@ export function ServerSettingsPage() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Event</Label>
|
||||
<Select value={automationEvent} onValueChange={(value) => setAutomationEvent(value as AutomationEvent)}>
|
||||
<Select
|
||||
value={automationEvent}
|
||||
onValueChange={(value) => setAutomationEvent(value as AutomationEvent)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -560,7 +574,9 @@ export function ServerSettingsPage() {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => automationRunMutation.mutate({ event: automationEvent, force: forceAutomationRun })}
|
||||
onClick={() =>
|
||||
automationRunMutation.mutate({ event: automationEvent, force: forceAutomationRun })
|
||||
}
|
||||
disabled={automationRunMutation.isPending}
|
||||
>
|
||||
{automationRunMutation.isPending ? 'Running...' : 'Run Automation Event'}
|
||||
@@ -601,8 +617,12 @@ export function ServerSettingsPage() {
|
||||
<p className="text-sm font-medium text-destructive">Failure Details</p>
|
||||
<div className="space-y-1">
|
||||
{lastAutomationResult.failures.slice(0, 5).map((failure, index) => (
|
||||
<p key={`${failure.workflowId}-${failure.actionId ?? 'workflow'}-${index}`} className="text-xs text-destructive">
|
||||
[{failure.workflowId}{failure.actionId ? ` > ${failure.actionId}` : ''}] {failure.message}
|
||||
<p
|
||||
key={`${failure.workflowId}-${failure.actionId ?? 'workflow'}-${index}`}
|
||||
className="text-xs text-destructive"
|
||||
>
|
||||
[{failure.workflowId}
|
||||
{failure.actionId ? ` > ${failure.actionId}` : ''}] {failure.message}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
@@ -611,20 +631,22 @@ export function ServerSettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{automationRunMutation.isSuccess && lastAutomationResult && lastAutomationResult.failures.length === 0 && (
|
||||
<p className="text-xs text-green-600">Automation run completed successfully.</p>
|
||||
)}
|
||||
{automationRunMutation.isSuccess &&
|
||||
lastAutomationResult &&
|
||||
lastAutomationResult.failures.length === 0 && (
|
||||
<p className="text-xs text-green-600">Automation run completed successfully.</p>
|
||||
)}
|
||||
|
||||
{automationRunMutation.isSuccess && lastAutomationResult && lastAutomationResult.failures.length > 0 && (
|
||||
<p className="text-xs text-destructive">
|
||||
Automation run completed with {lastAutomationResult.failures.length} error(s).
|
||||
</p>
|
||||
)}
|
||||
{automationRunMutation.isSuccess &&
|
||||
lastAutomationResult &&
|
||||
lastAutomationResult.failures.length > 0 && (
|
||||
<p className="text-xs text-destructive">
|
||||
Automation run completed with {lastAutomationResult.failures.length} error(s).
|
||||
</p>
|
||||
)}
|
||||
|
||||
{automationRunMutation.isError && (
|
||||
<p className="text-xs text-destructive">
|
||||
Failed to run automation event.
|
||||
</p>
|
||||
<p className="text-xs text-destructive">Failed to run automation event.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -639,7 +661,9 @@ export function ServerSettingsPage() {
|
||||
variant="destructive"
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={() => {
|
||||
if (!window.confirm('Delete this server permanently? This action cannot be undone.')) {
|
||||
if (
|
||||
!window.confirm('Delete this server permanently? This action cannot be undone.')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
deleteMutation.mutate();
|
||||
|
||||
@@ -16,7 +16,13 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
interface Member {
|
||||
id: string;
|
||||
@@ -78,21 +84,14 @@ export function MembersPage() {
|
||||
});
|
||||
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: (memberId: string) =>
|
||||
api.delete(`/organizations/${orgId}/members/${memberId}`),
|
||||
mutationFn: (memberId: string) => api.delete(`/organizations/${orgId}/members/${memberId}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['members', orgId] });
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({
|
||||
memberId,
|
||||
preset,
|
||||
}: {
|
||||
memberId: string;
|
||||
preset: MembershipPreset;
|
||||
}) =>
|
||||
mutationFn: ({ memberId, preset }: { memberId: string; preset: MembershipPreset }) =>
|
||||
api.patch(`/organizations/${orgId}/members/${memberId}`, buildPresetPayload(preset)),
|
||||
onMutate: ({ memberId }) => {
|
||||
setUpdatingMemberId(memberId);
|
||||
|
||||
Reference in New Issue
Block a user