mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +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:
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 { Badge } from '@/components/ui/Badge';
|
||||
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 { cn } from '@/lib/utils';
|
||||
import { CliEndpointFormDialog, type CliEndpointFormMode, type CliEndpointSavePayload } from '@/components/cli-endpoints/CliEndpointFormDialog';
|
||||
|
||||
// ========== Endpoint Card Component ==========
|
||||
|
||||
@@ -37,7 +39,7 @@ interface EndpointCardProps {
|
||||
onToggleExpand: () => void;
|
||||
onToggle: (endpointId: string, enabled: boolean) => void;
|
||||
onEdit: (endpoint: CliEndpoint) => void;
|
||||
onDelete: (endpointId: string) => void;
|
||||
onDelete: (endpointId: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit, onDelete }: EndpointCardProps) {
|
||||
@@ -94,6 +96,7 @@ function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit,
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
aria-label={formatMessage({ id: 'cliEndpoints.actions.toggle' })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle(endpoint.id, !endpoint.enabled);
|
||||
@@ -105,6 +108,7 @@ function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit,
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
aria-label={formatMessage({ id: 'cliEndpoints.actions.edit' })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(endpoint);
|
||||
@@ -116,6 +120,7 @@ function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit,
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
aria-label={formatMessage({ id: 'cliEndpoints.actions.delete' })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(endpoint.id);
|
||||
@@ -152,9 +157,13 @@ function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit,
|
||||
|
||||
export function EndpointsPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const { success, error: showError } = useNotifications();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState<'all' | 'litellm' | 'custom' | 'wrapper'>('all');
|
||||
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 {
|
||||
endpoints,
|
||||
@@ -168,6 +177,9 @@ export function EndpointsPage() {
|
||||
} = useCliEndpoints();
|
||||
|
||||
const { toggleEndpoint } = useToggleCliEndpoint();
|
||||
const { createEndpoint, isCreating } = useCreateCliEndpoint();
|
||||
const { updateEndpoint, isUpdating } = useUpdateCliEndpoint();
|
||||
const { deleteEndpoint, isDeleting } = useDeleteCliEndpoint();
|
||||
|
||||
const toggleExpand = (endpointId: string) => {
|
||||
setExpandedEndpoints((prev) => {
|
||||
@@ -185,16 +197,45 @@ export function EndpointsPage() {
|
||||
toggleEndpoint(endpointId, enabled);
|
||||
};
|
||||
|
||||
const handleDelete = (endpointId: string) => {
|
||||
if (confirm(formatMessage({ id: 'cliEndpoints.deleteConfirm' }, { id: endpointId }))) {
|
||||
// TODO: Implement delete functionality
|
||||
console.log('Delete endpoint:', endpointId);
|
||||
}
|
||||
const handleAdd = () => {
|
||||
setDialogMode('create');
|
||||
setEditingEndpoint(undefined);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (endpoint: CliEndpoint) => {
|
||||
// TODO: Implement edit dialog
|
||||
console.log('Edit endpoint:', endpoint);
|
||||
setDialogMode('edit');
|
||||
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
|
||||
@@ -234,7 +275,7 @@ export function EndpointsPage() {
|
||||
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
|
||||
{formatMessage({ id: 'common.actions.refresh' })}
|
||||
</Button>
|
||||
<Button>
|
||||
<Button onClick={handleAdd} disabled={isCreating || isUpdating || isDeleting}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'cliEndpoints.actions.add' })}
|
||||
</Button>
|
||||
@@ -327,6 +368,14 @@ export function EndpointsPage() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CliEndpointFormDialog
|
||||
mode={dialogMode}
|
||||
endpoint={editingEndpoint}
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onSave={handleDialogSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user