This commit is contained in:
2025-09-01 17:17:27 -04:00
parent aa524d8ac7
commit 74b25eba27
33 changed files with 669 additions and 686 deletions

2
kms/web/dist/665.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,15 @@
import React from 'react';
import {
FormSidebar,
FormField
Sidebar,
FormField,
Stack,
Group,
Button,
TextInput,
Select,
MultiSelect,
useForm,
notifications
} from '@skybridge/web-components';
import { apiService, Application, CreateApplicationRequest } from '../services/apiService';
@ -18,113 +26,149 @@ const ApplicationSidebar: React.FC<ApplicationSidebarProps> = ({
onSuccess,
editingApp,
}) => {
const form = useForm({
initialValues: {
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: '24h',
max_token_duration: '168h',
},
validate: {
app_id: (value) => value.length < 1 ? 'Application ID is required' : null,
app_link: (value) => value.length < 1 ? 'Application Link is required' : null,
type: (value) => value.length < 1 ? 'Application Type 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]?)$/);
if (!match) return 86400; // Default to 24h in seconds
if (!match) return 86400;
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
case 'm': return value * 60;
case 'h': return value * 3600;
case 'd': return value * 86400;
default: return value * 3600;
}
};
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',
},
};
try {
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) {
await apiService.updateApplication(editingApp.app_id, submitData);
} else {
await apiService.createApplication(submitData);
if (editingApp) {
await apiService.updateApplication(editingApp.app_id, submitData);
} else {
await apiService.createApplication(submitData);
}
notifications.show({
title: 'Success',
message: `Application ${editingApp ? 'updated' : 'created'} successfully`,
color: 'green',
});
onSuccess();
onClose();
} catch (error) {
notifications.show({
title: 'Error',
message: `Failed to ${editingApp ? 'update' : 'create'} application`,
color: 'red',
});
}
};
const footer = (
<Group justify="flex-end" gap="sm">
<Button variant="light" onClick={onClose}>
Cancel
</Button>
<Button onClick={form.onSubmit(handleSubmit)}>
{editingApp ? 'Update' : 'Create'} Application
</Button>
</Group>
);
return (
<FormSidebar
<Sidebar
opened={opened}
onClose={onClose}
onSuccess={onSuccess}
title="Application"
editMode={!!editingApp}
editItem={editingApp}
fields={fields}
onSubmit={handleSubmit}
width={450}
/>
title={editingApp ? 'Edit Application' : 'Create Application'}
layoutMode={true}
footer={footer}
>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<TextInput
label="Application ID"
placeholder="my-app-id"
required
disabled={!!editingApp}
{...form.getInputProps('app_id')}
/>
<TextInput
label="Application Link"
placeholder="https://myapp.example.com"
required
{...form.getInputProps('app_link')}
/>
<MultiSelect
label="Application Type"
placeholder="Select application types"
required
data={[
{ value: 'static', label: 'Static Token App' },
{ value: 'user', label: 'User Token App' },
]}
{...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')}
/>
<TextInput
label="Token Renewal Duration"
placeholder="24h"
{...form.getInputProps('token_renewal_duration')}
/>
<TextInput
label="Max Token Duration"
placeholder="168h"
{...form.getInputProps('max_token_duration')}
/>
</Stack>
</form>
</Sidebar>
);
};

View File

@ -5,7 +5,8 @@ import {
Badge,
Group,
Text,
Stack
SidebarLayout,
Sidebar
} from '@skybridge/web-components';
import { IconEye, IconCopy } from '@tabler/icons-react';
import { notifications } from '@mantine/notifications';
@ -123,7 +124,18 @@ const Applications: React.FC = () => {
];
return (
<Stack gap="md">
<SidebarLayout
sidebarOpened={sidebarOpen}
sidebarWidth={450}
sidebar={
<ApplicationSidebar
opened={sidebarOpen}
onClose={() => setSidebarOpen(false)}
onSuccess={handleSuccess}
editingApp={editingApp}
/>
}
>
<DataTable
data={applications}
columns={columns}
@ -137,14 +149,7 @@ const Applications: React.FC = () => {
customActions={customActions}
emptyMessage="No applications found"
/>
<ApplicationSidebar
opened={sidebarOpen}
onClose={() => setSidebarOpen(false)}
onSuccess={handleSuccess}
editingApp={editingApp}
/>
</Stack>
</SidebarLayout>
);
};

View File

@ -28,7 +28,7 @@ import {
IconRefresh,
} from '@tabler/icons-react';
import { DatePickerInput } from '@mantine/dates';
import { notifications } from '@mantine/notifications';
import { notifications, StatusBadge, LoadingState, EmptyState } from '@skybridge/web-components';
import {
apiService,
AuditEvent,
@ -124,19 +124,6 @@ const Audit: React.FC = () => {
});
};
const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'success':
return 'green';
case 'failure':
case 'error':
return 'red';
case 'warning':
return 'yellow';
default:
return 'gray';
}
};
const getEventTypeColor = (type: string) => {
if (type.startsWith('auth.')) return 'blue';
@ -166,9 +153,7 @@ const Audit: React.FC = () => {
</Badge>
</Table.Td>
<Table.Td>
<Badge color={getStatusColor(event.status)} variant="light" size="sm">
{event.status}
</Badge>
<StatusBadge value={event.status} size="sm" />
</Table.Td>
<Table.Td>
<Text size="sm" c="dimmed">
@ -360,9 +345,7 @@ const Audit: React.FC = () => {
<Group justify="space-between">
<Text fw={500}>Status:</Text>
<Badge color={getStatusColor(selectedEvent.status)} variant="light">
{selectedEvent.status}
</Badge>
<StatusBadge value={selectedEvent.status} />
</Group>
<Group justify="space-between">