-
This commit is contained in:
2
kms/web/dist/665.js
vendored
2
kms/web/dist/665.js
vendored
File diff suppressed because one or more lines are too long
2
kms/web/dist/main.js
vendored
2
kms/web/dist/main.js
vendored
File diff suppressed because one or more lines are too long
10
kms/web/dist/main.js.LICENSE.txt
vendored
10
kms/web/dist/main.js.LICENSE.txt
vendored
@ -1,3 +1,13 @@
|
||||
/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @remix-run/router v1.23.0
|
||||
*
|
||||
|
||||
2
kms/web/dist/remoteEntry.js
vendored
2
kms/web/dist/remoteEntry.js
vendored
File diff suppressed because one or more lines are too long
@ -16,9 +16,11 @@
|
||||
"@mantine/notifications": "^7.0.0",
|
||||
"@mantine/dates": "^7.0.0",
|
||||
"@mantine/form": "^7.0.0",
|
||||
"@mantine/modals": "^7.0.0",
|
||||
"@tabler/icons-react": "^2.40.0",
|
||||
"axios": "^1.11.0",
|
||||
"dayjs": "^1.11.13"
|
||||
"dayjs": "^1.11.13",
|
||||
"@skybridge/web-components": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
|
||||
@ -1,19 +1,8 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Paper,
|
||||
TextInput,
|
||||
MultiSelect,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Title,
|
||||
ActionIcon,
|
||||
ScrollArea,
|
||||
Box,
|
||||
} from '@mantine/core';
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import React from 'react';
|
||||
import {
|
||||
FormSidebar,
|
||||
FormField
|
||||
} from '@skybridge/web-components';
|
||||
import { apiService, Application, CreateApplicationRequest } from '../services/apiService';
|
||||
|
||||
interface ApplicationSidebarProps {
|
||||
@ -29,35 +18,6 @@ const ApplicationSidebar: React.FC<ApplicationSidebarProps> = ({
|
||||
onSuccess,
|
||||
editingApp,
|
||||
}) => {
|
||||
const isEditing = !!editingApp;
|
||||
|
||||
const appTypeOptions = [
|
||||
{ value: 'static', label: 'Static Token App' },
|
||||
{ value: 'user', label: 'User Token App' },
|
||||
];
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
const parseDuration = (duration: string): number => {
|
||||
// Convert duration string like "24h" to seconds
|
||||
const match = duration.match(/^(\d+)([hmd]?)$/);
|
||||
@ -74,167 +34,97 @@ const ApplicationSidebar: React.FC<ApplicationSidebarProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Update form values when editingApp changes
|
||||
useEffect(() => {
|
||||
const fields: FormField[] = [
|
||||
{
|
||||
name: 'app_id',
|
||||
label: 'Application ID',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'my-app-id',
|
||||
disabled: !!editingApp, // Disable editing for existing apps
|
||||
},
|
||||
{
|
||||
name: 'app_link',
|
||||
label: 'Application Link',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'https://myapp.example.com',
|
||||
validation: { url: true },
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Application Type',
|
||||
type: 'multiselect',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'static', label: 'Static Token App' },
|
||||
{ value: 'user', label: 'User Token App' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'callback_url',
|
||||
label: 'Callback URL',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'https://myapp.example.com/callback',
|
||||
validation: { url: true },
|
||||
},
|
||||
{
|
||||
name: 'token_prefix',
|
||||
label: 'Token Prefix (Optional)',
|
||||
type: 'text',
|
||||
required: false,
|
||||
placeholder: 'myapp_',
|
||||
},
|
||||
{
|
||||
name: 'token_renewal_duration',
|
||||
label: 'Token Renewal Duration',
|
||||
type: 'text',
|
||||
required: false,
|
||||
placeholder: '24h',
|
||||
defaultValue: '24h',
|
||||
},
|
||||
{
|
||||
name: 'max_token_duration',
|
||||
label: 'Max Token Duration',
|
||||
type: 'text',
|
||||
required: false,
|
||||
placeholder: '168h',
|
||||
defaultValue: '168h',
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
const submitData = {
|
||||
...values,
|
||||
token_renewal_duration_seconds: parseDuration(values.token_renewal_duration || '24h'),
|
||||
max_token_duration_seconds: parseDuration(values.max_token_duration || '168h'),
|
||||
owner: {
|
||||
type: 'individual',
|
||||
name: 'Admin User',
|
||||
owner: 'admin@example.com',
|
||||
},
|
||||
};
|
||||
|
||||
if (editingApp) {
|
||||
form.setValues({
|
||||
app_id: editingApp.app_id || '',
|
||||
app_link: editingApp.app_link || '',
|
||||
type: editingApp.type || [],
|
||||
callback_url: editingApp.callback_url || '',
|
||||
token_prefix: editingApp.token_prefix || '',
|
||||
token_renewal_duration: editingApp.token_renewal_duration || '24h',
|
||||
max_token_duration: editingApp.max_token_duration || '168h',
|
||||
owner: {
|
||||
type: editingApp.owner?.type || 'individual',
|
||||
name: editingApp.owner?.name || 'Admin User',
|
||||
owner: editingApp.owner?.owner || 'admin@example.com',
|
||||
},
|
||||
});
|
||||
await apiService.updateApplication(editingApp.app_id, submitData);
|
||||
} else {
|
||||
// Reset to default values when not editing
|
||||
form.reset();
|
||||
}
|
||||
}, [editingApp, opened]);
|
||||
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
try {
|
||||
const submitData = {
|
||||
...values,
|
||||
token_renewal_duration_seconds: parseDuration(values.token_renewal_duration),
|
||||
max_token_duration_seconds: parseDuration(values.max_token_duration),
|
||||
};
|
||||
|
||||
if (isEditing && editingApp) {
|
||||
await apiService.updateApplication(editingApp.app_id, submitData);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Application updated successfully',
|
||||
color: 'green',
|
||||
});
|
||||
} else {
|
||||
await apiService.createApplication(submitData);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Application created successfully',
|
||||
color: 'green',
|
||||
});
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
onClose();
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
console.error('Error saving application:', error);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: `Failed to ${isEditing ? 'update' : 'create'} application`,
|
||||
color: 'red',
|
||||
});
|
||||
await apiService.createApplication(submitData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 60, // Below header
|
||||
right: opened ? 0 : '-450px',
|
||||
bottom: 0,
|
||||
width: '450px',
|
||||
zIndex: 1000,
|
||||
borderRadius: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderLeft: '1px solid var(--mantine-color-gray-3)',
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
transition: 'right 0.3s ease',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Group justify="space-between" p="md" style={{ borderBottom: '1px solid var(--mantine-color-gray-3)' }}>
|
||||
<Title order={4}>
|
||||
{isEditing ? 'Edit Application' : 'Create New Application'}
|
||||
</Title>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={onClose}
|
||||
>
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollArea style={{ flex: 1 }}>
|
||||
<Box p="md">
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Application ID"
|
||||
placeholder="my-app-id"
|
||||
required
|
||||
{...form.getInputProps('app_id')}
|
||||
disabled={isEditing}
|
||||
/>
|
||||
|
||||
<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="light"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{isEditing ? 'Update Application' : 'Create Application'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
</Paper>
|
||||
<FormSidebar
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
title="Application"
|
||||
editMode={!!editingApp}
|
||||
editItem={editingApp}
|
||||
fields={fields}
|
||||
onSubmit={handleSubmit}
|
||||
width={450}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user