This commit is contained in:
2025-09-01 13:10:35 -04:00
parent d4f4747fde
commit aa524d8ac7
30 changed files with 731 additions and 1691 deletions

View File

@ -1,34 +1,15 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Button,
Stack,
Title,
Modal,
TextInput,
MultiSelect,
Group,
ActionIcon,
import {
DataTable,
TableColumn,
Badge,
Card,
Group,
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';
Stack
} from '@skybridge/web-components';
import { IconEye, IconCopy } from '@tabler/icons-react';
import { notifications } from '@mantine/notifications';
import { apiService, Application, CreateApplicationRequest } from '../services/apiService';
import { apiService, Application } from '../services/apiService';
import ApplicationSidebar from './ApplicationSidebar';
import dayjs from 'dayjs';
@ -37,30 +18,6 @@ const Applications: React.FC = () => {
const [loading, setLoading] = useState(false);
const [sidebarOpen, setSidebarOpen] = 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: 'individual',
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();
@ -73,109 +30,30 @@ const Applications: React.FC = () => {
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 parseDuration = (duration: string): number => {
// Convert duration string like "24h" to seconds
const match = duration.match(/^(\d+)([hmd]?)$/);
if (!match) return 86400; // Default to 24h in seconds
const value = parseInt(match[1]);
const unit = match[2] || 'h';
switch (unit) {
case 'm': return value * 60; // minutes to seconds
case 'h': return value * 3600; // hours to seconds
case 'd': return value * 86400; // days to seconds
default: return value * 3600; // default to hours
}
};
const handleSubmit = async (values: CreateApplicationRequest) => {
try {
// Convert duration strings to seconds for API
const apiValues = {
...values,
token_renewal_duration: parseDuration(values.token_renewal_duration),
max_token_duration: parseDuration(values.max_token_duration),
};
if (editingApp) {
await apiService.updateApplication(editingApp.app_id, apiValues);
notifications.show({
title: 'Success',
message: 'Application updated successfully',
color: 'green',
});
} else {
await apiService.createApplication(apiValues);
notifications.show({
title: 'Success',
message: 'Application created successfully',
color: 'green',
});
}
setSidebarOpen(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 handleAdd = () => {
setEditingApp(null);
setSidebarOpen(true);
};
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,
});
setSidebarOpen(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 handleDelete = async (app: Application) => {
await apiService.deleteApplication(app.app_id);
loadApplications();
};
const handleViewDetails = (app: Application) => {
setSelectedApp(app);
setDetailModalOpen(true);
const handleSuccess = () => {
setSidebarOpen(false);
setEditingApp(null);
loadApplications();
};
const copyToClipboard = (text: string) => {
@ -187,216 +65,85 @@ const Applications: React.FC = () => {
});
};
const appTypeOptions = [
{ value: 'static', label: 'Static' },
{ value: 'user', label: 'User' },
];
const rows = applications.map((app) => (
<Table.Tr key={app.app_id}>
<Table.Td>
<Text fw={500}>{app.app_id}</Text>
</Table.Td>
<Table.Td>
const columns: TableColumn[] = [
{
key: 'app_id',
label: 'Application ID',
render: (value) => <Text fw={500}>{value}</Text>
},
{
key: 'type',
label: 'Type',
render: (value: string[]) => (
<Group gap="xs">
{app.type.map((type) => (
{value.map((type) => (
<Badge key={type} variant="light" size="sm">
{type}
</Badge>
))}
</Group>
</Table.Td>
<Table.Td>
)
},
{
key: 'owner',
label: 'Owner',
render: (value: any) => (
<Text size="sm" c="dimmed">
{app.owner.name} ({app.owner.owner})
{value.name} ({value.owner})
</Text>
</Table.Td>
<Table.Td>
)
},
{
key: 'created_at',
label: 'Created',
render: (value) => (
<Text size="sm">
{dayjs(app.created_at).format('MMM DD, YYYY')}
{dayjs(value).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>
));
)
},
];
const customActions = [
{
key: 'view',
label: 'View Details',
icon: <IconEye size={14} />,
onClick: (app: Application) => {
// Could open a modal or navigate to details page
console.log('View details for:', app.app_id);
},
},
{
key: 'copy',
label: 'Copy App ID',
icon: <IconCopy size={14} />,
onClick: (app: Application) => copyToClipboard(app.app_id),
},
];
return (
<Stack
gap="lg"
style={{
transition: 'margin-right 0.3s ease',
marginRight: sidebarOpen ? '450px' : '0',
}}
>
<Group justify="space-between">
<div>
<Title order={2} mb="xs">
Applications
</Title>
</div>
<Button
leftSection={<IconPlus size={16} />}
onClick={() => {
setEditingApp(null);
form.reset();
setSidebarOpen(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();
setSidebarOpen(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>
)}
<Stack gap="md">
<DataTable
data={applications}
columns={columns}
loading={loading}
title="Applications"
searchable
onAdd={handleAdd}
onEdit={handleEdit}
onDelete={handleDelete}
onRefresh={loadApplications}
customActions={customActions}
emptyMessage="No applications found"
/>
<ApplicationSidebar
opened={sidebarOpen}
onClose={() => {
setSidebarOpen(false);
setEditingApp(null);
}}
onSuccess={() => {
loadApplications();
}}
onClose={() => setSidebarOpen(false)}
onSuccess={handleSuccess}
editingApp={editingApp}
/>
{/* 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>
);
};