449 lines
13 KiB
TypeScript
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; |