Files
skybridge/kms/kms-frontend/src/components/Applications.tsx
2025-08-26 19:16:41 -04:00

533 lines
16 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Table,
Button,
Space,
Typography,
Modal,
Form,
Input,
Select,
message,
Popconfirm,
Tag,
Card,
Row,
Col,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
CopyOutlined,
} from '@ant-design/icons';
import { apiService, Application, CreateApplicationRequest } from '../services/apiService';
import dayjs from 'dayjs';
const { Title, Text } = Typography;
const { Option } = Select;
const Applications: React.FC = () => {
const [applications, setApplications] = useState<Application[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [editingApp, setEditingApp] = useState<Application | null>(null);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [selectedApp, setSelectedApp] = useState<Application | null>(null);
const [form] = Form.useForm();
useEffect(() => {
loadApplications();
}, []);
const loadApplications = async () => {
try {
setLoading(true);
const response = await apiService.getApplications();
setApplications(response.data);
} catch (error) {
console.error('Failed to load applications:', error);
message.error('Failed to load applications');
} finally {
setLoading(false);
}
};
const handleCreate = () => {
setEditingApp(null);
form.resetFields();
setModalVisible(true);
};
const handleEdit = (app: Application) => {
setEditingApp(app);
form.setFieldsValue({
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: formatDuration(app.token_renewal_duration),
max_token_duration: formatDuration(app.max_token_duration),
owner_type: app.owner.type,
owner_name: app.owner.name,
owner_owner: app.owner.owner,
});
setModalVisible(true);
};
const handleDelete = async (appId: string) => {
try {
await apiService.deleteApplication(appId);
message.success('Application deleted successfully');
loadApplications();
} catch (error) {
console.error('Failed to delete application:', error);
message.error('Failed to delete application');
}
};
const handleSubmit = async (values: any) => {
try {
const requestData: CreateApplicationRequest = {
app_id: values.app_id,
app_link: values.app_link,
type: values.type,
callback_url: values.callback_url,
token_prefix: values.token_prefix,
token_renewal_duration: values.token_renewal_duration,
max_token_duration: values.max_token_duration,
owner: {
type: values.owner_type,
name: values.owner_name,
owner: values.owner_owner,
},
};
if (editingApp) {
await apiService.updateApplication(editingApp.app_id, requestData);
message.success('Application updated successfully');
} else {
await apiService.createApplication(requestData);
message.success('Application created successfully');
}
setModalVisible(false);
loadApplications();
} catch (error) {
console.error('Failed to save application:', error);
message.error('Failed to save application');
}
};
const showDetails = (app: Application) => {
setSelectedApp(app);
setDetailModalVisible(true);
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
message.success('Copied to clipboard');
};
const formatDuration = (nanoseconds: number): string => {
const hours = Math.floor(nanoseconds / (1000000000 * 60 * 60));
return `${hours}h`;
};
const columns = [
{
title: 'App ID',
dataIndex: 'app_id',
key: 'app_id',
render: (text: string) => <Text code>{text}</Text>,
},
{
title: 'App Link',
dataIndex: 'app_link',
key: 'app_link',
render: (text: string) => (
<a href={text} target="_blank" rel="noopener noreferrer">
{text}
</a>
),
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
render: (types: string[]) => (
<>
{types.map((type) => (
<Tag key={type} color={type === 'static' ? 'blue' : 'green'}>
{type.toUpperCase()}
</Tag>
))}
</>
),
},
{
title: 'Token Prefix',
dataIndex: 'token_prefix',
key: 'token_prefix',
render: (prefix: string) => prefix ? <Text code>{prefix}</Text> : <Text type="secondary">Default</Text>,
},
{
title: 'Owner',
dataIndex: 'owner',
key: 'owner',
render: (owner: Application['owner']) => (
<div>
<div>{owner.name}</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{owner.type} {owner.owner}
</Text>
</div>
),
},
{
title: 'Created',
dataIndex: 'created_at',
key: 'created_at',
render: (date: string) => dayjs(date).format('MMM DD, YYYY'),
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: Application) => (
<Space>
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => showDetails(record)}
title="View Details"
/>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
title="Edit"
/>
<Popconfirm
title="Are you sure you want to delete this application?"
onConfirm={() => handleDelete(record.app_id)}
okText="Yes"
cancelText="No"
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
title="Delete"
/>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Title level={2}>Applications</Title>
<Text type="secondary">
Manage your applications and their configurations
</Text>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
Create Application
</Button>
</div>
<Table
columns={columns}
dataSource={applications}
rowKey="app_id"
loading={loading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} applications`,
}}
/>
</Space>
{/* Create/Edit Modal */}
<Modal
title={editingApp ? 'Edit Application' : 'Create Application'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={() => form.submit()}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
name="app_id"
label="Application ID"
rules={[{ required: true, message: 'Please enter application ID' }]}
>
<Input placeholder="com.example.app" disabled={!!editingApp} />
</Form.Item>
<Form.Item
name="app_link"
label="Application Link"
rules={[
{ required: true, message: 'Please enter application link' },
{ type: 'url', message: 'Please enter a valid URL' },
]}
>
<Input placeholder="https://example.com" />
</Form.Item>
<Form.Item
name="type"
label="Application Type"
rules={[{ required: true, message: 'Please select application type' }]}
>
<Select mode="multiple" placeholder="Select types">
<Option value="static">Static</Option>
<Option value="user">User</Option>
</Select>
</Form.Item>
<Form.Item
name="callback_url"
label="Callback URL"
rules={[
{ required: true, message: 'Please enter callback URL' },
{ type: 'url', message: 'Please enter a valid URL' },
]}
>
<Input placeholder="https://example.com/callback" />
</Form.Item>
<Form.Item
name="token_prefix"
label="Token Prefix"
rules={[
{
pattern: /^[A-Z]{2,4}$/,
message: 'Token prefix must be 2-4 uppercase letters (e.g., NC for Nerd Completion)'
},
]}
help="Optional custom prefix for tokens. Leave empty for default 'kms_' prefix. Examples: NC → NCT- (static), NCUT- (user)"
>
<Input
placeholder="NC"
maxLength={4}
style={{ textTransform: 'uppercase' }}
onChange={(e) => {
const value = e.target.value.toUpperCase();
form.setFieldValue('token_prefix', value);
}}
/>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="token_renewal_duration"
label="Token Renewal Duration"
rules={[{ required: true, message: 'Please enter duration' }]}
>
<Input placeholder="168h" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="max_token_duration"
label="Max Token Duration"
rules={[{ required: true, message: 'Please enter duration' }]}
>
<Input placeholder="720h" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="owner_type"
label="Owner Type"
rules={[{ required: true, message: 'Please select owner type' }]}
>
<Select placeholder="Select owner type">
<Option value="individual">Individual</Option>
<Option value="team">Team</Option>
</Select>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="owner_name"
label="Owner Name"
rules={[{ required: true, message: 'Please enter owner name' }]}
>
<Input placeholder="John Doe" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="owner_owner"
label="Owner Contact"
rules={[{ required: true, message: 'Please enter owner contact' }]}
>
<Input placeholder="john.doe@example.com" />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
{/* Details Modal */}
<Modal
title="Application Details"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>
Close
</Button>,
]}
width={700}
>
{selectedApp && (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card title="Basic Information">
<Row gutter={16}>
<Col span={12}>
<Text strong>App ID:</Text>
<div>
<Text code>{selectedApp.app_id}</Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(selectedApp.app_id)}
/>
</div>
</Col>
<Col span={12}>
<Text strong>App Link:</Text>
<div>
<a href={selectedApp.app_link} target="_blank" rel="noopener noreferrer">
{selectedApp.app_link}
</a>
</div>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col span={12}>
<Text strong>Type:</Text>
<div>
{selectedApp.type.map((type) => (
<Tag key={type} color={type === 'static' ? 'blue' : 'green'}>
{type.toUpperCase()}
</Tag>
))}
</div>
</Col>
<Col span={12}>
<Text strong>Callback URL:</Text>
<div>{selectedApp.callback_url}</div>
</Col>
</Row>
</Card>
<Card title="Security Configuration">
<Row gutter={16}>
<Col span={12}>
<Text strong>HMAC Key:</Text>
<div>
<Text code></Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(selectedApp.hmac_key)}
/>
</div>
</Col>
<Col span={12}>
<Text strong>Token Prefix:</Text>
<div>
{selectedApp.token_prefix ? (
<>
<Text code>{selectedApp.token_prefix}</Text>
<Text type="secondary" style={{ marginLeft: 8 }}>
(Static: {selectedApp.token_prefix}T-, User: {selectedApp.token_prefix}UT-)
</Text>
</>
) : (
<Text type="secondary">Default (kms_)</Text>
)}
</div>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col span={12}>
<Text strong>Token Renewal Duration:</Text>
<div>{formatDuration(selectedApp.token_renewal_duration)}</div>
</Col>
<Col span={12}>
<Text strong>Max Token Duration:</Text>
<div>{formatDuration(selectedApp.max_token_duration)}</div>
</Col>
</Row>
</Card>
<Card title="Owner Information">
<Row gutter={16}>
<Col span={8}>
<Text strong>Type:</Text>
<div>
<Tag color={selectedApp.owner.type === 'individual' ? 'blue' : 'green'}>
{selectedApp.owner.type.toUpperCase()}
</Tag>
</div>
</Col>
<Col span={8}>
<Text strong>Name:</Text>
<div>{selectedApp.owner.name}</div>
</Col>
<Col span={8}>
<Text strong>Contact:</Text>
<div>{selectedApp.owner.owner}</div>
</Col>
</Row>
</Card>
<Card title="Timestamps">
<Row gutter={16}>
<Col span={12}>
<Text strong>Created:</Text>
<div>{dayjs(selectedApp.created_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
<Col span={12}>
<Text strong>Updated:</Text>
<div>{dayjs(selectedApp.updated_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
</Row>
</Card>
</Space>
)}
</Modal>
</div>
);
};
export default Applications;