unified UI thing
This commit is contained in:
449
kms/web/src/components/Applications.tsx
Normal file
449
kms/web/src/components/Applications.tsx
Normal file
@ -0,0 +1,449 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user