chore: initial commit for phase06
This commit is contained in:
@@ -25,6 +25,7 @@ import { ConsolePage } from '@/pages/server/console';
|
||||
import { FilesPage } from '@/pages/server/files';
|
||||
import { BackupsPage } from '@/pages/server/backups';
|
||||
import { SchedulesPage } from '@/pages/server/schedules';
|
||||
import { ConfigPage } from '@/pages/server/config';
|
||||
import { PluginsPage } from '@/pages/server/plugins';
|
||||
import { PlayersPage } from '@/pages/server/players';
|
||||
import { ServerSettingsPage } from '@/pages/server/settings';
|
||||
@@ -92,9 +93,10 @@ export function App() {
|
||||
<Route index element={<Navigate to="console" replace />} />
|
||||
<Route path="console" element={<ConsolePage />} />
|
||||
<Route path="files" element={<FilesPage />} />
|
||||
<Route path="config" element={<ConfigPage />} />
|
||||
<Route path="plugins" element={<PluginsPage />} />
|
||||
<Route path="backups" element={<BackupsPage />} />
|
||||
<Route path="schedules" element={<SchedulesPage />} />
|
||||
<Route path="plugins" element={<PluginsPage />} />
|
||||
<Route path="players" element={<PlayersPage />} />
|
||||
<Route path="settings" element={<ServerSettingsPage />} />
|
||||
</Route>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Outlet, useParams, Link, useLocation } from 'react-router';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Terminal, FolderOpen, Settings, Calendar, HardDrive, Users, Puzzle } from 'lucide-react';
|
||||
import { Terminal, FolderOpen, Settings, Calendar, HardDrive, Users, Puzzle, Settings2 } from 'lucide-react';
|
||||
import { cn } from '@source/ui';
|
||||
import { api } from '@/lib/api';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -25,9 +25,10 @@ interface ServerDetail {
|
||||
const tabs = [
|
||||
{ label: 'Console', path: 'console', icon: Terminal },
|
||||
{ label: 'Files', path: 'files', icon: FolderOpen },
|
||||
{ label: 'Config', path: 'config', icon: Settings2 },
|
||||
{ label: 'Plugins', path: 'plugins', icon: Puzzle },
|
||||
{ label: 'Backups', path: 'backups', icon: HardDrive },
|
||||
{ label: 'Schedules', path: 'schedules', icon: Calendar },
|
||||
{ label: 'Plugins', path: 'plugins', icon: Puzzle },
|
||||
{ label: 'Players', path: 'players', icon: Users },
|
||||
{ label: 'Settings', path: 'settings', icon: Settings },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Settings2, FileText, Save } from 'lucide-react';
|
||||
import { api } from '@/lib/api';
|
||||
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 { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
|
||||
interface ConfigFile {
|
||||
index: number;
|
||||
path: string;
|
||||
parser: string;
|
||||
editableKeys: string[] | null;
|
||||
}
|
||||
|
||||
interface ConfigEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ConfigDetail {
|
||||
path: string;
|
||||
parser: string;
|
||||
editableKeys: string[] | null;
|
||||
entries: ConfigEntry[];
|
||||
raw: string;
|
||||
}
|
||||
|
||||
export function ConfigPage() {
|
||||
const { orgId, serverId } = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: configsData } = useQuery({
|
||||
queryKey: ['configs', orgId, serverId],
|
||||
queryFn: () =>
|
||||
api.get<{ configs: ConfigFile[] }>(
|
||||
`/organizations/${orgId}/servers/${serverId}/config`,
|
||||
),
|
||||
});
|
||||
|
||||
const configs = configsData?.configs ?? [];
|
||||
|
||||
if (configs.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<Settings2 className="mb-4 h-12 w-12 text-muted-foreground/50" />
|
||||
<p className="text-muted-foreground">No config files available for this game</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="0" className="space-y-4">
|
||||
<TabsList>
|
||||
{configs.map((cf) => (
|
||||
<TabsTrigger key={cf.index} value={String(cf.index)}>
|
||||
<FileText className="mr-1.5 h-3.5 w-3.5" />
|
||||
{cf.path.split('/').pop()}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{configs.map((cf) => (
|
||||
<TabsContent key={cf.index} value={String(cf.index)}>
|
||||
<ConfigEditor
|
||||
orgId={orgId!}
|
||||
serverId={serverId!}
|
||||
configIndex={cf.index}
|
||||
configFile={cf}
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigEditor({
|
||||
orgId,
|
||||
serverId,
|
||||
configIndex,
|
||||
configFile,
|
||||
}: {
|
||||
orgId: string;
|
||||
serverId: string;
|
||||
configIndex: number;
|
||||
configFile: ConfigFile;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: detail } = useQuery({
|
||||
queryKey: ['config-detail', orgId, serverId, configIndex],
|
||||
queryFn: () =>
|
||||
api.get<ConfigDetail>(
|
||||
`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`,
|
||||
),
|
||||
});
|
||||
|
||||
const [entries, setEntries] = useState<ConfigEntry[]>([]);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
// Initialize entries from server data
|
||||
if (detail && !initialized) {
|
||||
setEntries(detail.entries);
|
||||
setInitialized(true);
|
||||
}
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (data: { entries: ConfigEntry[] }) =>
|
||||
api.patch(
|
||||
`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`,
|
||||
data,
|
||||
),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['config-detail', orgId, serverId, configIndex],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateEntry = (key: string, value: string) => {
|
||||
setEntries((prev) =>
|
||||
prev.map((e) => (e.key === key ? { ...e, value } : e)),
|
||||
);
|
||||
};
|
||||
|
||||
const displayEntries = configFile.editableKeys
|
||||
? entries.filter((e) => configFile.editableKeys!.includes(e.key))
|
||||
: entries;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{configFile.path}
|
||||
<Badge variant="outline">{configFile.parser}</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{configFile.editableKeys
|
||||
? `${configFile.editableKeys.length} editable keys`
|
||||
: 'All keys editable'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => saveMutation.mutate({ entries })}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
{saveMutation.isPending ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{displayEntries.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...'}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{displayEntries.map((entry) => (
|
||||
<div key={entry.key} className="grid gap-1.5">
|
||||
<Label className="font-mono text-xs text-muted-foreground">
|
||||
{entry.key}
|
||||
</Label>
|
||||
<Input
|
||||
value={entry.value}
|
||||
onChange={(e) => updateEntry(entry.key, e.target.value)}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{saveMutation.isSuccess && (
|
||||
<p className="mt-4 text-sm text-green-500">Config saved successfully</p>
|
||||
)}
|
||||
{saveMutation.isError && (
|
||||
<p className="mt-4 text-sm text-destructive">Failed to save config</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,81 @@
|
||||
import { Users } from 'lucide-react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Users, RefreshCw } from 'lucide-react';
|
||||
import { api } from '@/lib/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
|
||||
interface Player {
|
||||
name: string;
|
||||
steamid?: string;
|
||||
}
|
||||
|
||||
interface PlayerListResponse {
|
||||
players: Player[];
|
||||
maxPlayers: number;
|
||||
}
|
||||
|
||||
export function PlayersPage() {
|
||||
const { orgId, serverId } = useParams();
|
||||
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey: ['players', orgId, serverId],
|
||||
queryFn: () =>
|
||||
api.get<PlayerListResponse>(
|
||||
`/organizations/${orgId}/servers/${serverId}/players`,
|
||||
),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const players = data?.players ?? [];
|
||||
const maxPlayers = data?.maxPlayers ?? 0;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<Users className="mb-4 h-12 w-12 text-muted-foreground/50" />
|
||||
<p className="text-muted-foreground">Active player tracking coming soon</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Active Players</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{players.length} / {maxPlayers} players online
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()} disabled={isLoading}>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{players.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<Users className="mb-4 h-12 w-12 text-muted-foreground/50" />
|
||||
<p className="text-muted-foreground">No players online</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Player tracking requires RCON to be enabled on the server
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y">
|
||||
{players.map((player, i) => (
|
||||
<div key={i} className="flex items-center gap-3 px-4 py-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-sm font-medium text-primary">
|
||||
{player.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{player.name}</p>
|
||||
{player.steamid && (
|
||||
<p className="text-xs text-muted-foreground">{player.steamid}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,323 @@
|
||||
import { Puzzle } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Puzzle,
|
||||
Search,
|
||||
Download,
|
||||
Trash2,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
Star,
|
||||
Upload,
|
||||
} from 'lucide-react';
|
||||
import { api } from '@/lib/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
interface InstalledPlugin {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
source: 'spiget' | 'manual';
|
||||
externalId: string | null;
|
||||
installedVersion: string | null;
|
||||
isActive: boolean;
|
||||
installedAt: string;
|
||||
}
|
||||
|
||||
interface SpigetResult {
|
||||
id: number;
|
||||
name: string;
|
||||
tag: string;
|
||||
downloads: number;
|
||||
rating: { average: number; count: number };
|
||||
updateDate: number;
|
||||
external: boolean;
|
||||
}
|
||||
|
||||
export function PluginsPage() {
|
||||
const { orgId, serverId } = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: pluginsData } = useQuery({
|
||||
queryKey: ['plugins', orgId, serverId],
|
||||
queryFn: () =>
|
||||
api.get<{ plugins: InstalledPlugin[] }>(
|
||||
`/organizations/${orgId}/servers/${serverId}/plugins`,
|
||||
),
|
||||
});
|
||||
|
||||
const installed = pluginsData?.plugins ?? [];
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="installed" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="installed">
|
||||
<Puzzle className="mr-1.5 h-3.5 w-3.5" />
|
||||
Installed ({installed.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="search">
|
||||
<Search className="mr-1.5 h-3.5 w-3.5" />
|
||||
Search Plugins
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="manual">
|
||||
<Upload className="mr-1.5 h-3.5 w-3.5" />
|
||||
Manual Install
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="installed">
|
||||
<InstalledPlugins installed={installed} orgId={orgId!} serverId={serverId!} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="search">
|
||||
<SpigetSearch orgId={orgId!} serverId={serverId!} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manual">
|
||||
<ManualInstall orgId={orgId!} serverId={serverId!} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
function InstalledPlugins({
|
||||
installed,
|
||||
orgId,
|
||||
serverId,
|
||||
}: {
|
||||
installed: InstalledPlugin[];
|
||||
orgId: string;
|
||||
serverId: string;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
api.patch(`/organizations/${orgId}/servers/${serverId}/plugins/${id}/toggle`),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }),
|
||||
});
|
||||
|
||||
const uninstallMutation = useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
api.delete(`/organizations/${orgId}/servers/${serverId}/plugins/${id}`),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }),
|
||||
});
|
||||
|
||||
if (installed.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<Puzzle className="mb-4 h-12 w-12 text-muted-foreground/50" />
|
||||
<p className="text-muted-foreground">No plugins installed</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Search for plugins or install manually
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{installed.map((plugin) => (
|
||||
<Card key={plugin.id}>
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Puzzle className="h-5 w-5 text-primary" />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium">{plugin.name}</p>
|
||||
<Badge variant="outline">{plugin.source}</Badge>
|
||||
{!plugin.isActive && <Badge variant="secondary">Disabled</Badge>}
|
||||
</div>
|
||||
{plugin.description && (
|
||||
<p className="text-sm text-muted-foreground">{plugin.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => toggleMutation.mutate(plugin.id)}
|
||||
title={plugin.isActive ? 'Disable' : 'Enable'}
|
||||
>
|
||||
{plugin.isActive ? (
|
||||
<ToggleRight className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<ToggleLeft className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => uninstallMutation.mutate(plugin.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SpigetSearch({ orgId, serverId }: { orgId: string; serverId: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [query, setQuery] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const { data: results, isLoading } = useQuery({
|
||||
queryKey: ['spiget-search', orgId, serverId, searchTerm],
|
||||
queryFn: () =>
|
||||
api.get<{ results: SpigetResult[] }>(
|
||||
`/organizations/${orgId}/servers/${serverId}/plugins/search`,
|
||||
{ q: searchTerm },
|
||||
),
|
||||
enabled: searchTerm.length >= 2,
|
||||
});
|
||||
|
||||
const installMutation = useMutation({
|
||||
mutationFn: (resourceId: number) =>
|
||||
api.post(`/organizations/${orgId}/servers/${serverId}/plugins/install/spiget`, {
|
||||
resourceId,
|
||||
}),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }),
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
if (query.length >= 2) setSearchTerm(query);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Search Spiget plugins (Minecraft only)..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
<Button onClick={handleSearch} disabled={query.length < 2}>
|
||||
<Search className="h-4 w-4" />
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Searching...</p>}
|
||||
|
||||
{results?.results && results.results.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">No results found</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{results?.results?.map((r) => (
|
||||
<Card key={r.id}>
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<div>
|
||||
<p className="font-medium">{r.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{r.tag}</p>
|
||||
<div className="mt-1 flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Star className="h-3 w-3" />
|
||||
{r.rating.average.toFixed(1)} ({r.rating.count})
|
||||
</span>
|
||||
<span>
|
||||
<Download className="inline h-3 w-3" /> {r.downloads.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => installMutation.mutate(r.id)}
|
||||
disabled={installMutation.isPending || r.external}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
{r.external ? 'External' : 'Install'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ManualInstall({ orgId, serverId }: { orgId: string; serverId: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [name, setName] = useState('');
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [version, setVersion] = useState('');
|
||||
|
||||
const installMutation = useMutation({
|
||||
mutationFn: (body: { name: string; fileName: string; version?: string }) =>
|
||||
api.post(`/organizations/${orgId}/servers/${serverId}/plugins/install/manual`, body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] });
|
||||
setName('');
|
||||
setFileName('');
|
||||
setVersion('');
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<Puzzle className="mb-4 h-12 w-12 text-muted-foreground/50" />
|
||||
<p className="text-muted-foreground">Plugin management coming soon</p>
|
||||
<CardHeader>
|
||||
<CardTitle>Manual Plugin Install</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
installMutation.mutate({
|
||||
name,
|
||||
fileName,
|
||||
version: version || undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Label>Plugin Name</Label>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>File Name</Label>
|
||||
<Input
|
||||
value={fileName}
|
||||
onChange={(e) => setFileName(e.target.value)}
|
||||
placeholder="plugin.jar"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Upload the file to /plugins/ directory via the Files tab first
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Version (optional)</Label>
|
||||
<Input value={version} onChange={(e) => setVersion(e.target.value)} />
|
||||
</div>
|
||||
<Button type="submit" disabled={installMutation.isPending}>
|
||||
{installMutation.isPending ? 'Registering...' : 'Register Plugin'}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user