mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat(cli-endpoints): add create, update, and delete functionality for CLI endpoints
- Implemented `useCreateCliEndpoint`, `useUpdateCliEndpoint`, and `useDeleteCliEndpoint` hooks for managing CLI endpoints. - Added `CliEndpointFormDialog` component for creating and editing CLI endpoints with validation. - Updated translations for CLI hooks and manager to include new fields and messages. - Refactored `CcwToolsMcpCard` to simplify enabling and disabling tools. - Adjusted `SkillCreateDialog` to display paths based on CLI type.
This commit is contained in:
@@ -0,0 +1,270 @@
|
|||||||
|
// ========================================
|
||||||
|
// CLI Endpoint Form Dialog
|
||||||
|
// ========================================
|
||||||
|
// Dialog for creating and editing CLI endpoints
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/Dialog';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { Textarea } from '@/components/ui/Textarea';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Label } from '@/components/ui/Label';
|
||||||
|
import { Switch } from '@/components/ui/Switch';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/Select';
|
||||||
|
import type { CliEndpoint } from '@/lib/api';
|
||||||
|
|
||||||
|
export type CliEndpointFormMode = 'create' | 'edit';
|
||||||
|
|
||||||
|
export interface CliEndpointSavePayload {
|
||||||
|
name: string;
|
||||||
|
type: CliEndpoint['type'];
|
||||||
|
enabled: boolean;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CliEndpointFormDialogProps {
|
||||||
|
mode: CliEndpointFormMode;
|
||||||
|
endpoint?: CliEndpoint;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (payload: CliEndpointSavePayload) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormErrors {
|
||||||
|
name?: string;
|
||||||
|
type?: string;
|
||||||
|
configJson?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeStringifyConfig(config: unknown): string {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(config ?? {}, null, 2);
|
||||||
|
} catch {
|
||||||
|
return '{}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfigJson(
|
||||||
|
configJson: string
|
||||||
|
): { ok: true; value: Record<string, unknown> } | { ok: false; errorKey: string } {
|
||||||
|
const trimmed = configJson.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return { ok: true, value: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed) as unknown;
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
|
return { ok: false, errorKey: 'validation.configMustBeObject' };
|
||||||
|
}
|
||||||
|
return { ok: true, value: parsed as Record<string, unknown> };
|
||||||
|
} catch {
|
||||||
|
return { ok: false, errorKey: 'validation.invalidJson' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CliEndpointFormDialog({
|
||||||
|
mode,
|
||||||
|
endpoint,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
}: CliEndpointFormDialogProps) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const isEditing = mode === 'edit';
|
||||||
|
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [type, setType] = useState<CliEndpoint['type']>('custom');
|
||||||
|
const [enabled, setEnabled] = useState(true);
|
||||||
|
const [configJson, setConfigJson] = useState('{}');
|
||||||
|
const [errors, setErrors] = useState<FormErrors>({});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const dialogTitle = useMemo(() => {
|
||||||
|
return isEditing
|
||||||
|
? formatMessage({ id: 'cliEndpoints.dialog.editTitle' }, { id: endpoint?.id ?? '' })
|
||||||
|
: formatMessage({ id: 'cliEndpoints.dialog.createTitle' });
|
||||||
|
}, [formatMessage, isEditing, endpoint?.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
if (isEditing && endpoint) {
|
||||||
|
setName(endpoint.name);
|
||||||
|
setType(endpoint.type);
|
||||||
|
setEnabled(endpoint.enabled);
|
||||||
|
setConfigJson(safeStringifyConfig(endpoint.config));
|
||||||
|
} else {
|
||||||
|
setName('');
|
||||||
|
setType('custom');
|
||||||
|
setEnabled(true);
|
||||||
|
setConfigJson('{}');
|
||||||
|
}
|
||||||
|
setErrors({});
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}, [open, isEditing, endpoint]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const nextErrors: FormErrors = {};
|
||||||
|
|
||||||
|
if (!name.trim()) {
|
||||||
|
nextErrors.name = 'validation.nameRequired';
|
||||||
|
}
|
||||||
|
if (!type) {
|
||||||
|
nextErrors.type = 'validation.typeRequired';
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedConfig = parseConfigJson(configJson);
|
||||||
|
if (!parsedConfig.ok) {
|
||||||
|
nextErrors.configJson = parsedConfig.errorKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(nextErrors).length > 0) {
|
||||||
|
setErrors(nextErrors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await onSave({
|
||||||
|
name: name.trim(),
|
||||||
|
type,
|
||||||
|
enabled,
|
||||||
|
config: (parsedConfig as { ok: true; value: Record<string, unknown> }).value,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save CLI endpoint:', err);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="sm:max-w-[720px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{dialogTitle}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
{isEditing && endpoint && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cli-endpoint-id">{formatMessage({ id: 'cliEndpoints.id' })}</Label>
|
||||||
|
<Input
|
||||||
|
id="cli-endpoint-id"
|
||||||
|
value={endpoint.id}
|
||||||
|
disabled
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cli-endpoint-name">
|
||||||
|
{formatMessage({ id: 'cliEndpoints.form.name' })} *
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="cli-endpoint-name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => {
|
||||||
|
setName(e.target.value);
|
||||||
|
if (errors.name) setErrors((prev) => ({ ...prev, name: undefined }));
|
||||||
|
}}
|
||||||
|
placeholder={formatMessage({ id: 'cliEndpoints.form.namePlaceholder' })}
|
||||||
|
error={!!errors.name}
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{formatMessage({ id: `cliEndpoints.${errors.name}` })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cli-endpoint-type">
|
||||||
|
{formatMessage({ id: 'cliEndpoints.form.type' })} *
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={type}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setType(v as CliEndpoint['type']);
|
||||||
|
if (errors.type) setErrors((prev) => ({ ...prev, type: undefined }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="cli-endpoint-type" className={errors.type ? 'border-destructive' : undefined}>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="litellm">{formatMessage({ id: 'cliEndpoints.type.litellm' })}</SelectItem>
|
||||||
|
<SelectItem value="custom">{formatMessage({ id: 'cliEndpoints.type.custom' })}</SelectItem>
|
||||||
|
<SelectItem value="wrapper">{formatMessage({ id: 'cliEndpoints.type.wrapper' })}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errors.type && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{formatMessage({ id: `cliEndpoints.${errors.type}` })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between rounded-md border border-border p-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-foreground">{formatMessage({ id: 'cliEndpoints.form.enabled' })}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{formatMessage({ id: 'cliEndpoints.form.enabledHint' })}</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cli-endpoint-config">{formatMessage({ id: 'cliEndpoints.form.configJson' })}</Label>
|
||||||
|
<Textarea
|
||||||
|
id="cli-endpoint-config"
|
||||||
|
value={configJson}
|
||||||
|
onChange={(e) => {
|
||||||
|
setConfigJson(e.target.value);
|
||||||
|
if (errors.configJson) setErrors((prev) => ({ ...prev, configJson: undefined }));
|
||||||
|
}}
|
||||||
|
placeholder={formatMessage({ id: 'cliEndpoints.form.configJsonPlaceholder' })}
|
||||||
|
className={errors.configJson ? 'font-mono border-destructive' : 'font-mono'}
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
{errors.configJson && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{formatMessage({ id: `cliEndpoints.${errors.configJson}` })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose} disabled={isSubmitting}>
|
||||||
|
{formatMessage({ id: 'common.actions.cancel' })}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||||
|
{isSubmitting
|
||||||
|
? formatMessage({ id: 'common.actions.saving' })
|
||||||
|
: formatMessage({ id: 'common.actions.save' })}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CliEndpointFormDialog;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -151,17 +151,12 @@ export function CcwToolsMcpCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEnableAll = () => {
|
const handleEnableAll = () => {
|
||||||
CCW_MCP_TOOLS.forEach((tool) => {
|
const allToolNames = CCW_MCP_TOOLS.map((t) => t.name);
|
||||||
if (!enabledTools.includes(tool.name)) {
|
onUpdateConfig({ enabledTools: allToolNames });
|
||||||
onToggleTool(tool.name, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDisableAll = () => {
|
const handleDisableAll = () => {
|
||||||
enabledTools.forEach((toolName) => {
|
onUpdateConfig({ enabledTools: [] });
|
||||||
onToggleTool(toolName, false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfigSave = () => {
|
const handleConfigSave = () => {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export function SkillCreateDialog({ open, onOpenChange, onCreated, cliType = 'cl
|
|||||||
<Folder className="w-5 h-5" />
|
<Folder className="w-5 h-5" />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-sm">{formatMessage({ id: 'skills.create.locationProject' })}</div>
|
<div className="font-medium text-sm">{formatMessage({ id: 'skills.create.locationProject' })}</div>
|
||||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'skills.create.locationProjectHint' })}</div>
|
<div className="text-xs text-muted-foreground">{`.${cliType}/skills/`}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -195,7 +195,7 @@ export function SkillCreateDialog({ open, onOpenChange, onCreated, cliType = 'cl
|
|||||||
<User className="w-5 h-5" />
|
<User className="w-5 h-5" />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-sm">{formatMessage({ id: 'skills.create.locationUser' })}</div>
|
<div className="font-medium text-sm">{formatMessage({ id: 'skills.create.locationUser' })}</div>
|
||||||
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'skills.create.locationUserHint' })}</div>
|
<div className="text-xs text-muted-foreground">{`~/.${cliType}/skills/`}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ export type {
|
|||||||
export {
|
export {
|
||||||
useCliEndpoints,
|
useCliEndpoints,
|
||||||
useToggleCliEndpoint,
|
useToggleCliEndpoint,
|
||||||
|
useCreateCliEndpoint,
|
||||||
|
useUpdateCliEndpoint,
|
||||||
|
useDeleteCliEndpoint,
|
||||||
cliEndpointsKeys,
|
cliEndpointsKeys,
|
||||||
useCliInstallations,
|
useCliInstallations,
|
||||||
useInstallCliTool,
|
useInstallCliTool,
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import { sanitizeErrorMessage } from '../utils/errorSanitizer';
|
|||||||
import {
|
import {
|
||||||
fetchCliEndpoints,
|
fetchCliEndpoints,
|
||||||
toggleCliEndpoint,
|
toggleCliEndpoint,
|
||||||
|
createCliEndpoint,
|
||||||
|
updateCliEndpoint,
|
||||||
|
deleteCliEndpoint,
|
||||||
type CliEndpoint,
|
type CliEndpoint,
|
||||||
type CliEndpointsResponse,
|
type CliEndpointsResponse,
|
||||||
} from '../lib/api';
|
} from '../lib/api';
|
||||||
@@ -121,6 +124,101 @@ export function useToggleCliEndpoint() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCreateCliEndpoint() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (endpoint: Omit<CliEndpoint, 'id'>) => createCliEndpoint(endpoint),
|
||||||
|
onSuccess: (created) => {
|
||||||
|
queryClient.setQueryData<CliEndpointsResponse>(cliEndpointsKeys.lists(), (old) => {
|
||||||
|
if (!old) return { endpoints: [created] };
|
||||||
|
return { endpoints: [created, ...old.endpoints] };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: cliEndpointsKeys.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
createEndpoint: mutation.mutateAsync,
|
||||||
|
isCreating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateCliEndpoint() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: ({ endpointId, updates }: { endpointId: string; updates: Partial<CliEndpoint> }) =>
|
||||||
|
updateCliEndpoint(endpointId, updates),
|
||||||
|
onMutate: async ({ endpointId, updates }) => {
|
||||||
|
await queryClient.cancelQueries({ queryKey: cliEndpointsKeys.all });
|
||||||
|
const previous = queryClient.getQueryData<CliEndpointsResponse>(cliEndpointsKeys.lists());
|
||||||
|
|
||||||
|
queryClient.setQueryData<CliEndpointsResponse>(cliEndpointsKeys.lists(), (old) => {
|
||||||
|
if (!old) return old;
|
||||||
|
return {
|
||||||
|
endpoints: old.endpoints.map((e) => (e.id === endpointId ? { ...e, ...updates } : e)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { previous };
|
||||||
|
},
|
||||||
|
onError: (_err, _vars, context) => {
|
||||||
|
if (context?.previous) {
|
||||||
|
queryClient.setQueryData(cliEndpointsKeys.lists(), context.previous);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: cliEndpointsKeys.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateEndpoint: (endpointId: string, updates: Partial<CliEndpoint>) =>
|
||||||
|
mutation.mutateAsync({ endpointId, updates }),
|
||||||
|
isUpdating: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteCliEndpoint() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (endpointId: string) => deleteCliEndpoint(endpointId),
|
||||||
|
onMutate: async (endpointId) => {
|
||||||
|
await queryClient.cancelQueries({ queryKey: cliEndpointsKeys.all });
|
||||||
|
const previous = queryClient.getQueryData<CliEndpointsResponse>(cliEndpointsKeys.lists());
|
||||||
|
|
||||||
|
queryClient.setQueryData<CliEndpointsResponse>(cliEndpointsKeys.lists(), (old) => {
|
||||||
|
if (!old) return old;
|
||||||
|
return {
|
||||||
|
endpoints: old.endpoints.filter((e) => e.id !== endpointId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { previous };
|
||||||
|
},
|
||||||
|
onError: (_err, _endpointId, context) => {
|
||||||
|
if (context?.previous) {
|
||||||
|
queryClient.setQueryData(cliEndpointsKeys.lists(), context.previous);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: cliEndpointsKeys.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteEndpoint: mutation.mutateAsync,
|
||||||
|
isDeleting: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// useCliInstallations Hook
|
// useCliInstallations Hook
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
@@ -104,7 +104,10 @@
|
|||||||
"hookType": "Hook Type",
|
"hookType": "Hook Type",
|
||||||
"trigger": "Trigger Event",
|
"trigger": "Trigger Event",
|
||||||
"platform": "Platform",
|
"platform": "Platform",
|
||||||
"commandPreview": "Command Preview"
|
"commandPreview": "Command Preview",
|
||||||
|
"installTo": "Install To",
|
||||||
|
"scopeProject": "Project",
|
||||||
|
"scopeGlobal": "Global"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
@@ -117,36 +120,49 @@
|
|||||||
"title": "Memory Update Wizard",
|
"title": "Memory Update Wizard",
|
||||||
"description": "Configure hook to update CLAUDE.md on session end",
|
"description": "Configure hook to update CLAUDE.md on session end",
|
||||||
"shortDescription": "Update CLAUDE.md automatically",
|
"shortDescription": "Update CLAUDE.md automatically",
|
||||||
"claudePath": "CLAUDE.md Path",
|
"cliTool": "CLI Tool",
|
||||||
"updateFrequency": "Update Frequency",
|
"cliToolHelp": "CLI tool for CLAUDE.md generation",
|
||||||
"frequency": {
|
"threshold": "Threshold (paths)",
|
||||||
"sessionEnd": "Session End",
|
"thresholdHelp": "Number of paths to trigger batch update (1-20)",
|
||||||
"hourly": "Hourly",
|
"timeout": "Timeout (seconds)",
|
||||||
"daily": "Daily"
|
"timeoutHelp": "Auto-flush queue after this time (60-1800)"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"dangerProtection": {
|
"dangerProtection": {
|
||||||
"title": "Danger Protection Wizard",
|
"title": "Danger Protection Wizard",
|
||||||
"description": "Configure confirmation hook for dangerous operations",
|
"description": "Configure confirmation hook for dangerous operations",
|
||||||
"shortDescription": "Confirm dangerous operations",
|
"shortDescription": "Confirm dangerous operations",
|
||||||
"keywords": "Dangerous Keywords",
|
"selectProtections": "Select the protections you want to enable",
|
||||||
"keywordsHelp": "Enter one keyword per line",
|
"selectedProtections": "Selected Protections",
|
||||||
"confirmationMessage": "Confirmation Message",
|
"options": {
|
||||||
"allowBypass": "Allow bypass with --force flag"
|
"bashConfirm": "Dangerous Commands",
|
||||||
|
"bashConfirmDesc": "Confirm before rm -rf, shutdown, kill, format, etc.",
|
||||||
|
"fileProtection": "Sensitive Files",
|
||||||
|
"fileProtectionDesc": "Block modifications to .env, .git/, secrets, keys",
|
||||||
|
"gitDestructive": "Git Operations",
|
||||||
|
"gitDestructiveDesc": "Confirm force push, hard reset, branch delete",
|
||||||
|
"networkConfirm": "Network Access",
|
||||||
|
"networkConfirmDesc": "Confirm curl, wget, ssh, WebFetch requests",
|
||||||
|
"systemPaths": "System Paths",
|
||||||
|
"systemPathsDesc": "Block/confirm operations on /etc, /usr, C:\\Windows",
|
||||||
|
"permissionChange": "Permission Changes",
|
||||||
|
"permissionChangeDesc": "Confirm chmod, chown, icacls operations"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"skillContext": {
|
"skillContext": {
|
||||||
"title": "SKILL Context Wizard",
|
"title": "SKILL Context Wizard",
|
||||||
"description": "Configure hook to load SKILL based on prompt keywords",
|
"description": "Configure hook to load SKILL based on prompt keywords",
|
||||||
"shortDescription": "Auto-load SKILL based on keywords",
|
"shortDescription": "Auto-load SKILL based on keywords",
|
||||||
"loadingSkills": "Loading available skills...",
|
"loadingSkills": "Loading available skills...",
|
||||||
"keywordPlaceholder": "Enter keyword",
|
|
||||||
"selectSkill": "Select skill",
|
"selectSkill": "Select skill",
|
||||||
"addPair": "Add Keyword-Skill Pair",
|
"addPair": "Add Skill Configuration",
|
||||||
"priority": "Priority",
|
"keywordMappings": "Keyword Mappings",
|
||||||
"priorityHigh": "High",
|
"keywordsPlaceholder": "react,workflow,api",
|
||||||
"priorityMedium": "Medium",
|
"mode": "Detection Mode",
|
||||||
"priorityLow": "Low",
|
"modeKeyword": "Keyword Matching",
|
||||||
"keywordMappings": "Keyword Mappings"
|
"modeKeywordDesc": "Load specific SKILLs when keywords are detected",
|
||||||
|
"modeAuto": "Auto Detection",
|
||||||
|
"modeAutoDesc": "Automatically detect and load SKILLs by name",
|
||||||
|
"autoDescription": "Auto detection mode will scan user prompts for SKILL names and automatically load matching context. All available skills are shown below."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,32 @@
|
|||||||
"delete": "Delete Endpoint",
|
"delete": "Delete Endpoint",
|
||||||
"toggle": "Toggle Endpoint"
|
"toggle": "Toggle Endpoint"
|
||||||
},
|
},
|
||||||
|
"dialog": {
|
||||||
|
"createTitle": "Add Endpoint",
|
||||||
|
"editTitle": "Edit Endpoint ({id})"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"name": "Name",
|
||||||
|
"namePlaceholder": "My endpoint",
|
||||||
|
"type": "Type",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"enabledHint": "Enable or disable this endpoint",
|
||||||
|
"configJson": "Configuration (JSON)",
|
||||||
|
"configJsonPlaceholder": "{\n \n}"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"nameRequired": "Name is required",
|
||||||
|
"typeRequired": "Type is required",
|
||||||
|
"invalidJson": "Invalid JSON",
|
||||||
|
"configMustBeObject": "Configuration must be a JSON object"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"created": "Endpoint created",
|
||||||
|
"updated": "Endpoint updated",
|
||||||
|
"deleted": "Endpoint deleted",
|
||||||
|
"saveFailed": "Failed to save endpoint",
|
||||||
|
"deleteFailed": "Failed to delete endpoint"
|
||||||
|
},
|
||||||
"deleteConfirm": "Are you sure you want to delete the endpoint \"{id}\"?",
|
"deleteConfirm": "Are you sure you want to delete the endpoint \"{id}\"?",
|
||||||
"emptyState": {
|
"emptyState": {
|
||||||
"title": "No CLI Endpoints Found",
|
"title": "No CLI Endpoints Found",
|
||||||
|
|||||||
@@ -104,7 +104,10 @@
|
|||||||
"hookType": "钩子类型",
|
"hookType": "钩子类型",
|
||||||
"trigger": "触发事件",
|
"trigger": "触发事件",
|
||||||
"platform": "平台",
|
"platform": "平台",
|
||||||
"commandPreview": "命令预览"
|
"commandPreview": "命令预览",
|
||||||
|
"installTo": "安装到",
|
||||||
|
"scopeProject": "项目",
|
||||||
|
"scopeGlobal": "全局"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
@@ -117,36 +120,49 @@
|
|||||||
"title": "记忆更新向导",
|
"title": "记忆更新向导",
|
||||||
"description": "配置钩子以在会话结束时更新 CLAUDE.md",
|
"description": "配置钩子以在会话结束时更新 CLAUDE.md",
|
||||||
"shortDescription": "自动更新 CLAUDE.md",
|
"shortDescription": "自动更新 CLAUDE.md",
|
||||||
"claudePath": "CLAUDE.md 路径",
|
"cliTool": "CLI 工具",
|
||||||
"updateFrequency": "更新频率",
|
"cliToolHelp": "用于生成 CLAUDE.md 的 CLI 工具",
|
||||||
"frequency": {
|
"threshold": "阈值(路径数)",
|
||||||
"sessionEnd": "会话结束时",
|
"thresholdHelp": "触发批量更新的路径数量(1-20)",
|
||||||
"hourly": "每小时",
|
"timeout": "超时时间(秒)",
|
||||||
"daily": "每天"
|
"timeoutHelp": "超过此时间自动刷新队列(60-1800)"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"dangerProtection": {
|
"dangerProtection": {
|
||||||
"title": "危险操作保护向导",
|
"title": "危险操作保护向导",
|
||||||
"description": "配置危险操作的确认钩子",
|
"description": "配置危险操作的确认钩子",
|
||||||
"shortDescription": "确认危险操作",
|
"shortDescription": "确认危险操作",
|
||||||
"keywords": "危险关键词",
|
"selectProtections": "选择要启用的保护类型",
|
||||||
"keywordsHelp": "每行输入一个关键词",
|
"selectedProtections": "已选保护",
|
||||||
"confirmationMessage": "确认消息",
|
"options": {
|
||||||
"allowBypass": "允许使用 --force 标志绕过"
|
"bashConfirm": "危险命令",
|
||||||
|
"bashConfirmDesc": "执行 rm -rf、shutdown、kill、format 等命令前确认",
|
||||||
|
"fileProtection": "敏感文件",
|
||||||
|
"fileProtectionDesc": "阻止修改 .env、.git/、密钥等敏感文件",
|
||||||
|
"gitDestructive": "Git 操作",
|
||||||
|
"gitDestructiveDesc": "强制推送、硬重置、删除分支前确认",
|
||||||
|
"networkConfirm": "网络访问",
|
||||||
|
"networkConfirmDesc": "执行 curl、wget、ssh、WebFetch 请求前确认",
|
||||||
|
"systemPaths": "系统路径",
|
||||||
|
"systemPathsDesc": "阻止/确认对 /etc、/usr、C:\\Windows 的操作",
|
||||||
|
"permissionChange": "权限变更",
|
||||||
|
"permissionChangeDesc": "执行 chmod、chown、icacls 操作前确认"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"skillContext": {
|
"skillContext": {
|
||||||
"title": "SKILL 上下文向导",
|
"title": "SKILL 上下文向导",
|
||||||
"description": "配置钩子以根据提示关键词加载 SKILL",
|
"description": "配置钩子以根据提示关键词加载 SKILL",
|
||||||
"shortDescription": "根据关键词自动加载 SKILL",
|
"shortDescription": "根据关键词自动加载 SKILL",
|
||||||
"loadingSkills": "正在加载可用的技能...",
|
"loadingSkills": "正在加载可用的技能...",
|
||||||
"keywordPlaceholder": "输入关键词",
|
|
||||||
"selectSkill": "选择技能",
|
"selectSkill": "选择技能",
|
||||||
"addPair": "添加关键词-技能对",
|
"addPair": "添加技能配置",
|
||||||
"priority": "优先级",
|
"keywordMappings": "关键词映射",
|
||||||
"priorityHigh": "高",
|
"keywordsPlaceholder": "react,workflow,api",
|
||||||
"priorityMedium": "中",
|
"mode": "检测模式",
|
||||||
"priorityLow": "低",
|
"modeKeyword": "关键词匹配",
|
||||||
"keywordMappings": "关键词映射"
|
"modeKeywordDesc": "检测到关键词时加载对应 SKILL",
|
||||||
|
"modeAuto": "自动检测",
|
||||||
|
"modeAutoDesc": "自动检测并按名称加载 SKILL",
|
||||||
|
"autoDescription": "自动检测模式将扫描用户提示中的 SKILL 名称,并自动加载匹配的上下文。所有可用技能如下所示。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,32 @@
|
|||||||
"delete": "删除端点",
|
"delete": "删除端点",
|
||||||
"toggle": "切换端点"
|
"toggle": "切换端点"
|
||||||
},
|
},
|
||||||
|
"dialog": {
|
||||||
|
"createTitle": "添加端点",
|
||||||
|
"editTitle": "编辑端点({id})"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"name": "名称",
|
||||||
|
"namePlaceholder": "我的端点",
|
||||||
|
"type": "类型",
|
||||||
|
"enabled": "启用",
|
||||||
|
"enabledHint": "启用或禁用该端点",
|
||||||
|
"configJson": "配置(JSON)",
|
||||||
|
"configJsonPlaceholder": "{\n \n}"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"nameRequired": "名称不能为空",
|
||||||
|
"typeRequired": "类型不能为空",
|
||||||
|
"invalidJson": "JSON 格式不正确",
|
||||||
|
"configMustBeObject": "配置必须是 JSON 对象"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"created": "端点已创建",
|
||||||
|
"updated": "端点已更新",
|
||||||
|
"deleted": "端点已删除",
|
||||||
|
"saveFailed": "保存端点失败",
|
||||||
|
"deleteFailed": "删除端点失败"
|
||||||
|
},
|
||||||
"deleteConfirm": "确定要删除端点 \"{id}\" 吗?",
|
"deleteConfirm": "确定要删除端点 \"{id}\" 吗?",
|
||||||
"emptyState": {
|
"emptyState": {
|
||||||
"title": "未找到 CLI 端点",
|
"title": "未找到 CLI 端点",
|
||||||
|
|||||||
131
ccw/frontend/src/pages/EndpointsPage.test.tsx
Normal file
131
ccw/frontend/src/pages/EndpointsPage.test.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// ========================================
|
||||||
|
// Endpoints Page Tests
|
||||||
|
// ========================================
|
||||||
|
// Tests for the CLI endpoints page with i18n
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { render, screen, waitFor } from '@/test/i18n';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { EndpointsPage } from './EndpointsPage';
|
||||||
|
import type { CliEndpoint } from '@/lib/api';
|
||||||
|
|
||||||
|
const mockEndpoints: CliEndpoint[] = [
|
||||||
|
{
|
||||||
|
id: 'ep-1',
|
||||||
|
name: 'Endpoint 1',
|
||||||
|
type: 'custom',
|
||||||
|
enabled: true,
|
||||||
|
config: { foo: 'bar' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const createEndpointMock = vi.fn();
|
||||||
|
const updateEndpointMock = vi.fn();
|
||||||
|
const deleteEndpointMock = vi.fn();
|
||||||
|
const toggleEndpointMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('@/hooks', () => ({
|
||||||
|
useCliEndpoints: () => ({
|
||||||
|
endpoints: mockEndpoints,
|
||||||
|
litellmEndpoints: [],
|
||||||
|
customEndpoints: mockEndpoints,
|
||||||
|
wrapperEndpoints: [],
|
||||||
|
totalCount: mockEndpoints.length,
|
||||||
|
enabledCount: mockEndpoints.length,
|
||||||
|
isLoading: false,
|
||||||
|
isFetching: false,
|
||||||
|
error: null,
|
||||||
|
refetch: vi.fn(),
|
||||||
|
invalidate: vi.fn(),
|
||||||
|
}),
|
||||||
|
useToggleCliEndpoint: () => ({
|
||||||
|
toggleEndpoint: toggleEndpointMock,
|
||||||
|
isToggling: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
useCreateCliEndpoint: () => ({
|
||||||
|
createEndpoint: createEndpointMock,
|
||||||
|
isCreating: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
useUpdateCliEndpoint: () => ({
|
||||||
|
updateEndpoint: updateEndpointMock,
|
||||||
|
isUpdating: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
useDeleteCliEndpoint: () => ({
|
||||||
|
deleteEndpoint: deleteEndpointMock,
|
||||||
|
isDeleting: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/hooks/useNotifications', () => ({
|
||||||
|
useNotifications: () => ({
|
||||||
|
success: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('EndpointsPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
// confirm() used for delete
|
||||||
|
// @ts-expect-error - test override
|
||||||
|
global.confirm = vi.fn(() => true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render page title', () => {
|
||||||
|
render(<EndpointsPage />, { locale: 'en' });
|
||||||
|
expect(screen.getByText(/CLI Endpoints/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open create dialog and call createEndpoint', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
createEndpointMock.mockResolvedValueOnce({ id: 'ep-2' });
|
||||||
|
|
||||||
|
render(<EndpointsPage />, { locale: 'en' });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /Add Endpoint/i }));
|
||||||
|
|
||||||
|
expect(screen.getByText(/Add Endpoint/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.type(screen.getByLabelText(/^Name/i), 'New Endpoint');
|
||||||
|
await user.click(screen.getByRole('button', { name: /^Save$/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(createEndpointMock).toHaveBeenCalledWith({
|
||||||
|
name: 'New Endpoint',
|
||||||
|
type: 'custom',
|
||||||
|
enabled: true,
|
||||||
|
config: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open edit dialog when edit clicked', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
updateEndpointMock.mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
render(<EndpointsPage />, { locale: 'en' });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /Edit Endpoint/i }));
|
||||||
|
|
||||||
|
expect(screen.getByText(/Edit Endpoint/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText(/^ID$/i)).toHaveValue('ep-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call deleteEndpoint when delete confirmed', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
deleteEndpointMock.mockResolvedValueOnce(undefined);
|
||||||
|
|
||||||
|
render(<EndpointsPage />, { locale: 'en' });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /Delete Endpoint/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(deleteEndpointMock).toHaveBeenCalledWith('ep-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -25,9 +25,11 @@ import { Button } from '@/components/ui/Button';
|
|||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/Select';
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/Select';
|
||||||
import { useCliEndpoints, useToggleCliEndpoint } from '@/hooks';
|
import { useCliEndpoints, useCreateCliEndpoint, useDeleteCliEndpoint, useToggleCliEndpoint, useUpdateCliEndpoint } from '@/hooks';
|
||||||
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
import type { CliEndpoint } from '@/lib/api';
|
import type { CliEndpoint } from '@/lib/api';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { CliEndpointFormDialog, type CliEndpointFormMode, type CliEndpointSavePayload } from '@/components/cli-endpoints/CliEndpointFormDialog';
|
||||||
|
|
||||||
// ========== Endpoint Card Component ==========
|
// ========== Endpoint Card Component ==========
|
||||||
|
|
||||||
@@ -37,7 +39,7 @@ interface EndpointCardProps {
|
|||||||
onToggleExpand: () => void;
|
onToggleExpand: () => void;
|
||||||
onToggle: (endpointId: string, enabled: boolean) => void;
|
onToggle: (endpointId: string, enabled: boolean) => void;
|
||||||
onEdit: (endpoint: CliEndpoint) => void;
|
onEdit: (endpoint: CliEndpoint) => void;
|
||||||
onDelete: (endpointId: string) => void;
|
onDelete: (endpointId: string) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit, onDelete }: EndpointCardProps) {
|
function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit, onDelete }: EndpointCardProps) {
|
||||||
@@ -94,6 +96,7 @@ function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit,
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
|
aria-label={formatMessage({ id: 'cliEndpoints.actions.toggle' })}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggle(endpoint.id, !endpoint.enabled);
|
onToggle(endpoint.id, !endpoint.enabled);
|
||||||
@@ -105,6 +108,7 @@ function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit,
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
|
aria-label={formatMessage({ id: 'cliEndpoints.actions.edit' })}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEdit(endpoint);
|
onEdit(endpoint);
|
||||||
@@ -116,6 +120,7 @@ function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit,
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
|
aria-label={formatMessage({ id: 'cliEndpoints.actions.delete' })}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDelete(endpoint.id);
|
onDelete(endpoint.id);
|
||||||
@@ -152,9 +157,13 @@ function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit,
|
|||||||
|
|
||||||
export function EndpointsPage() {
|
export function EndpointsPage() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const { success, error: showError } = useNotifications();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [typeFilter, setTypeFilter] = useState<'all' | 'litellm' | 'custom' | 'wrapper'>('all');
|
const [typeFilter, setTypeFilter] = useState<'all' | 'litellm' | 'custom' | 'wrapper'>('all');
|
||||||
const [expandedEndpoints, setExpandedEndpoints] = useState<Set<string>>(new Set());
|
const [expandedEndpoints, setExpandedEndpoints] = useState<Set<string>>(new Set());
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [dialogMode, setDialogMode] = useState<CliEndpointFormMode>('create');
|
||||||
|
const [editingEndpoint, setEditingEndpoint] = useState<CliEndpoint | undefined>(undefined);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
endpoints,
|
endpoints,
|
||||||
@@ -168,6 +177,9 @@ export function EndpointsPage() {
|
|||||||
} = useCliEndpoints();
|
} = useCliEndpoints();
|
||||||
|
|
||||||
const { toggleEndpoint } = useToggleCliEndpoint();
|
const { toggleEndpoint } = useToggleCliEndpoint();
|
||||||
|
const { createEndpoint, isCreating } = useCreateCliEndpoint();
|
||||||
|
const { updateEndpoint, isUpdating } = useUpdateCliEndpoint();
|
||||||
|
const { deleteEndpoint, isDeleting } = useDeleteCliEndpoint();
|
||||||
|
|
||||||
const toggleExpand = (endpointId: string) => {
|
const toggleExpand = (endpointId: string) => {
|
||||||
setExpandedEndpoints((prev) => {
|
setExpandedEndpoints((prev) => {
|
||||||
@@ -185,16 +197,45 @@ export function EndpointsPage() {
|
|||||||
toggleEndpoint(endpointId, enabled);
|
toggleEndpoint(endpointId, enabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (endpointId: string) => {
|
const handleAdd = () => {
|
||||||
if (confirm(formatMessage({ id: 'cliEndpoints.deleteConfirm' }, { id: endpointId }))) {
|
setDialogMode('create');
|
||||||
// TODO: Implement delete functionality
|
setEditingEndpoint(undefined);
|
||||||
console.log('Delete endpoint:', endpointId);
|
setDialogOpen(true);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (endpoint: CliEndpoint) => {
|
const handleEdit = (endpoint: CliEndpoint) => {
|
||||||
// TODO: Implement edit dialog
|
setDialogMode('edit');
|
||||||
console.log('Edit endpoint:', endpoint);
|
setEditingEndpoint(endpoint);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (endpointId: string) => {
|
||||||
|
if (!confirm(formatMessage({ id: 'cliEndpoints.deleteConfirm' }, { id: endpointId }))) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteEndpoint(endpointId);
|
||||||
|
success(formatMessage({ id: 'cliEndpoints.messages.deleted' }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete CLI endpoint:', err);
|
||||||
|
showError(formatMessage({ id: 'cliEndpoints.messages.deleteFailed' }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogSave = async (payload: CliEndpointSavePayload) => {
|
||||||
|
try {
|
||||||
|
if (dialogMode === 'edit' && editingEndpoint) {
|
||||||
|
await updateEndpoint(editingEndpoint.id, payload);
|
||||||
|
success(formatMessage({ id: 'cliEndpoints.messages.updated' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createEndpoint(payload);
|
||||||
|
success(formatMessage({ id: 'cliEndpoints.messages.created' }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save CLI endpoint:', err);
|
||||||
|
showError(formatMessage({ id: 'cliEndpoints.messages.saveFailed' }));
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter endpoints by search query and type
|
// Filter endpoints by search query and type
|
||||||
@@ -234,7 +275,7 @@ export function EndpointsPage() {
|
|||||||
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
|
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
|
||||||
{formatMessage({ id: 'common.actions.refresh' })}
|
{formatMessage({ id: 'common.actions.refresh' })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button>
|
<Button onClick={handleAdd} disabled={isCreating || isUpdating || isDeleting}>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
{formatMessage({ id: 'cliEndpoints.actions.add' })}
|
{formatMessage({ id: 'cliEndpoints.actions.add' })}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -327,6 +368,14 @@ export function EndpointsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<CliEndpointFormDialog
|
||||||
|
mode={dialogMode}
|
||||||
|
endpoint={editingEndpoint}
|
||||||
|
open={dialogOpen}
|
||||||
|
onClose={() => setDialogOpen(false)}
|
||||||
|
onSave={handleDialogSave}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user