203 lines
5.7 KiB
TypeScript
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>
|
|
);
|
|
}
|