Files
skybridge/kms/web/src/components/Applications.tsx
2025-08-27 11:22:12 -04:00

449 lines
13 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Table,
Button,
Stack,
Title,
Modal,
TextInput,
MultiSelect,
Group,
ActionIcon,
Badge,
Card,
Text,
Loader,
Alert,
Textarea,
Select,
NumberInput,
} from '@mantine/core';
import {
IconPlus,
IconEdit,
IconTrash,
IconEye,
IconCopy,
IconAlertCircle,
} from '@tabler/icons-react';
import { useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import { apiService, Application, CreateApplicationRequest } from '../services/apiService';
import dayjs from 'dayjs';
const Applications: React.FC = () => {
const [applications, setApplications] = useState<Application[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingApp, setEditingApp] = useState<Application | null>(null);
const [detailModalOpen, setDetailModalOpen] = useState(false);
const [selectedApp, setSelectedApp] = useState<Application | null>(null);
const form = useForm<CreateApplicationRequest>({
initialValues: {
app_id: '',
app_link: '',
type: [],
callback_url: '',
token_prefix: '',
token_renewal_duration: '24h',
max_token_duration: '168h',
owner: {
type: 'user',
name: 'Admin User',
owner: 'admin@example.com',
},
},
validate: {
app_id: (value) => value.length < 1 ? 'App ID is required' : null,
app_link: (value) => value.length < 1 ? 'App Link is required' : null,
callback_url: (value) => value.length < 1 ? 'Callback URL is required' : null,
},
});
useEffect(() => {
loadApplications();
}, []);
const loadApplications = async () => {
try {
setLoading(true);
const response = await apiService.getApplications(100, 0);
setApplications(response.data);
} catch (error) {
console.error('Failed to load applications:', error);
notifications.show({
title: 'Error',
message: 'Failed to load applications',
color: 'red',
});
} finally {
setLoading(false);
}
};
const handleSubmit = async (values: CreateApplicationRequest) => {
try {
if (editingApp) {
await apiService.updateApplication(editingApp.app_id, values);
notifications.show({
title: 'Success',
message: 'Application updated successfully',
color: 'green',
});
} else {
await apiService.createApplication(values);
notifications.show({
title: 'Success',
message: 'Application created successfully',
color: 'green',
});
}
setModalOpen(false);
setEditingApp(null);
form.reset();
loadApplications();
} catch (error) {
console.error('Failed to save application:', error);
notifications.show({
title: 'Error',
message: 'Failed to save application',
color: 'red',
});
}
};
const handleEdit = (app: Application) => {
setEditingApp(app);
form.setValues({
app_id: app.app_id,
app_link: app.app_link,
type: app.type,
callback_url: app.callback_url,
token_prefix: app.token_prefix || '',
token_renewal_duration: `${app.token_renewal_duration / 3600}h`,
max_token_duration: `${app.max_token_duration / 3600}h`,
owner: app.owner,
});
setModalOpen(true);
};
const handleDelete = async (appId: string) => {
if (window.confirm('Are you sure you want to delete this application?')) {
try {
await apiService.deleteApplication(appId);
notifications.show({
title: 'Success',
message: 'Application deleted successfully',
color: 'green',
});
loadApplications();
} catch (error) {
console.error('Failed to delete application:', error);
notifications.show({
title: 'Error',
message: 'Failed to delete application',
color: 'red',
});
}
}
};
const handleViewDetails = (app: Application) => {
setSelectedApp(app);
setDetailModalOpen(true);
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
notifications.show({
title: 'Copied',
message: 'Copied to clipboard',
color: 'blue',
});
};
const appTypeOptions = [
{ value: 'web', label: 'Web Application' },
{ value: 'mobile', label: 'Mobile Application' },
{ value: 'api', label: 'API Service' },
{ value: 'cli', label: 'CLI Tool' },
{ value: 'service', label: 'Background Service' },
];
const rows = applications.map((app) => (
<Table.Tr key={app.app_id}>
<Table.Td>
<Text fw={500}>{app.app_id}</Text>
</Table.Td>
<Table.Td>
<Group gap="xs">
{app.type.map((type) => (
<Badge key={type} variant="light" size="sm">
{type}
</Badge>
))}
</Group>
</Table.Td>
<Table.Td>
<Text size="sm" c="dimmed">
{app.owner.name} ({app.owner.owner})
</Text>
</Table.Td>
<Table.Td>
<Text size="sm">
{dayjs(app.created_at).format('MMM DD, YYYY')}
</Text>
</Table.Td>
<Table.Td>
<Group gap="xs">
<ActionIcon
variant="subtle"
color="blue"
onClick={() => handleViewDetails(app)}
title="View Details"
>
<IconEye size={16} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="gray"
onClick={() => handleEdit(app)}
title="Edit"
>
<IconEdit size={16} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="red"
onClick={() => handleDelete(app.app_id)}
title="Delete"
>
<IconTrash size={16} />
</ActionIcon>
</Group>
</Table.Td>
</Table.Tr>
));
return (
<Stack gap="lg">
<Group justify="space-between">
<div>
<Title order={2} mb="xs">
Applications
</Title>
<Text c="dimmed">
Manage your registered applications and their configurations
</Text>
</div>
<Button
leftSection={<IconPlus size={16} />}
onClick={() => {
setEditingApp(null);
form.reset();
setModalOpen(true);
}}
>
New Application
</Button>
</Group>
{loading ? (
<Stack align="center" justify="center" h={200}>
<Loader size="lg" />
<Text>Loading applications...</Text>
</Stack>
) : applications.length === 0 ? (
<Card shadow="sm" radius="md" withBorder p="xl">
<Stack align="center" gap="md">
<IconAlertCircle size={48} color="gray" />
<div style={{ textAlign: 'center' }}>
<Text fw={500} mb="xs">
No applications found
</Text>
<Text size="sm" c="dimmed">
Create your first application to get started with the key management system
</Text>
</div>
<Button
leftSection={<IconPlus size={16} />}
onClick={() => {
setEditingApp(null);
form.reset();
setModalOpen(true);
}}
>
Create Application
</Button>
</Stack>
</Card>
) : (
<Card shadow="sm" radius="md" withBorder>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Application ID</Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Owner</Table.Th>
<Table.Th>Created</Table.Th>
<Table.Th>Actions</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Card>
)}
{/* Create/Edit Modal */}
<Modal
opened={modalOpen}
onClose={() => {
setModalOpen(false);
setEditingApp(null);
form.reset();
}}
title={editingApp ? 'Edit Application' : 'Create New Application'}
size="lg"
>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<TextInput
label="Application ID"
placeholder="my-app-id"
required
{...form.getInputProps('app_id')}
disabled={!!editingApp}
/>
<TextInput
label="Application Link"
placeholder="https://myapp.example.com"
required
{...form.getInputProps('app_link')}
/>
<MultiSelect
label="Application Type"
placeholder="Select application types"
data={appTypeOptions}
required
{...form.getInputProps('type')}
/>
<TextInput
label="Callback URL"
placeholder="https://myapp.example.com/callback"
required
{...form.getInputProps('callback_url')}
/>
<TextInput
label="Token Prefix (Optional)"
placeholder="myapp_"
{...form.getInputProps('token_prefix')}
/>
<Group grow>
<TextInput
label="Token Renewal Duration"
placeholder="24h"
{...form.getInputProps('token_renewal_duration')}
/>
<TextInput
label="Max Token Duration"
placeholder="168h"
{...form.getInputProps('max_token_duration')}
/>
</Group>
<Group justify="flex-end" mt="md">
<Button
variant="subtle"
onClick={() => {
setModalOpen(false);
setEditingApp(null);
form.reset();
}}
>
Cancel
</Button>
<Button type="submit">
{editingApp ? 'Update Application' : 'Create Application'}
</Button>
</Group>
</Stack>
</form>
</Modal>
{/* Detail Modal */}
<Modal
opened={detailModalOpen}
onClose={() => setDetailModalOpen(false)}
title="Application Details"
size="md"
>
{selectedApp && (
<Stack gap="md">
<Group justify="space-between">
<Text fw={500}>Application ID:</Text>
<Group gap="xs">
<Text>{selectedApp.app_id}</Text>
<ActionIcon
size="sm"
variant="subtle"
onClick={() => copyToClipboard(selectedApp.app_id)}
>
<IconCopy size={12} />
</ActionIcon>
</Group>
</Group>
<Group justify="space-between">
<Text fw={500}>HMAC Key:</Text>
<Group gap="xs">
<Text size="sm" style={{ fontFamily: 'monospace' }}>
{selectedApp.hmac_key.substring(0, 16)}...
</Text>
<ActionIcon
size="sm"
variant="subtle"
onClick={() => copyToClipboard(selectedApp.hmac_key)}
>
<IconCopy size={12} />
</ActionIcon>
</Group>
</Group>
<Group justify="space-between">
<Text fw={500}>Application Link:</Text>
<Text size="sm">{selectedApp.app_link}</Text>
</Group>
<Group justify="space-between">
<Text fw={500}>Callback URL:</Text>
<Text size="sm">{selectedApp.callback_url}</Text>
</Group>
<Group justify="space-between">
<Text fw={500}>Token Renewal:</Text>
<Text size="sm">{selectedApp.token_renewal_duration / 3600}h</Text>
</Group>
<Group justify="space-between">
<Text fw={500}>Max Duration:</Text>
<Text size="sm">{selectedApp.max_token_duration / 3600}h</Text>
</Group>
<Group justify="space-between">
<Text fw={500}>Created:</Text>
<Text size="sm">{dayjs(selectedApp.created_at).format('MMM DD, YYYY HH:mm')}</Text>
</Group>
</Stack>
)}
</Modal>
</Stack>
);
};
export default Applications;