Files
source-gamepanel/apps/web/src/pages/server/config.tsx
T

203 lines
5.7 KiB
TypeScript

import { useEffect, 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;
}
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]));
const merged = [...entries];
for (const key of editableKeys) {
if (!existing.has(key)) {
merged.push({ key, value: '' });
}
}
return merged;
}
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[]>([]);
useEffect(() => {
if (!detail) return;
setEntries(mergeConfigEntries(detail.entries, configFile.editableKeys));
}, [detail, configFile.editableKeys]);
const saveMutation = useMutation({
mutationFn: (data: { entries: ConfigEntry[] }) =>
api.put(
`/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)),
);
};
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} allowed additions, plus existing keys`
: 'All detected 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>
{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...'}
</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>
<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>
);
}