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