feat(frontend): add GlobalVariables management page
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
243
frontend/src/pages/GlobalVariables.tsx
Normal file
243
frontend/src/pages/GlobalVariables.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Checkbox,
|
||||
Space,
|
||||
Typography,
|
||||
Popconfirm,
|
||||
message,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '../api/client';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface GlobalVariable {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
value_type: 'string' | 'number' | 'boolean' | 'json';
|
||||
description: string | null;
|
||||
is_secret: boolean;
|
||||
}
|
||||
|
||||
interface VariableFormData {
|
||||
key: string;
|
||||
value: string;
|
||||
value_type: 'string' | 'number' | 'boolean' | 'json';
|
||||
description?: string;
|
||||
is_secret: boolean;
|
||||
}
|
||||
|
||||
export default function GlobalVariables() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingVariable, setEditingVariable] = useState<GlobalVariable | null>(null);
|
||||
const [form] = Form.useForm<VariableFormData>();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: variables, isLoading } = useQuery({
|
||||
queryKey: ['global-variables'],
|
||||
queryFn: () => apiClient.get<GlobalVariable[]>('/api/global-variables'),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: VariableFormData) =>
|
||||
apiClient.post('/api/global-variables', data),
|
||||
onSuccess: () => {
|
||||
message.success('Variable creada');
|
||||
queryClient.invalidateQueries({ queryKey: ['global-variables'] });
|
||||
handleCloseModal();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ key, data }: { key: string; data: Partial<VariableFormData> }) =>
|
||||
apiClient.put(`/api/global-variables/${key}`, data),
|
||||
onSuccess: () => {
|
||||
message.success('Variable actualizada');
|
||||
queryClient.invalidateQueries({ queryKey: ['global-variables'] });
|
||||
handleCloseModal();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (key: string) =>
|
||||
apiClient.delete(`/api/global-variables/${key}`),
|
||||
onSuccess: () => {
|
||||
message.success('Variable eliminada');
|
||||
queryClient.invalidateQueries({ queryKey: ['global-variables'] });
|
||||
},
|
||||
});
|
||||
|
||||
const handleOpenCreate = () => {
|
||||
setEditingVariable(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ value_type: 'string', is_secret: false });
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenEdit = (variable: GlobalVariable) => {
|
||||
setEditingVariable(variable);
|
||||
form.setFieldsValue({
|
||||
key: variable.key,
|
||||
value: variable.is_secret ? '' : variable.value,
|
||||
value_type: variable.value_type,
|
||||
description: variable.description || '',
|
||||
is_secret: variable.is_secret,
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setEditingVariable(null);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const values = await form.validateFields();
|
||||
if (editingVariable) {
|
||||
const updateData: Partial<VariableFormData> = {
|
||||
value_type: values.value_type,
|
||||
description: values.description,
|
||||
is_secret: values.is_secret,
|
||||
};
|
||||
if (values.value) {
|
||||
updateData.value = values.value;
|
||||
}
|
||||
updateMutation.mutate({ key: editingVariable.key, data: updateData });
|
||||
} else {
|
||||
createMutation.mutate(values);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Clave',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
render: (key: string) => <code>{key}</code>,
|
||||
},
|
||||
{
|
||||
title: 'Valor',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
render: (value: string, record: GlobalVariable) =>
|
||||
record.is_secret ? '••••••••' : value,
|
||||
},
|
||||
{
|
||||
title: 'Tipo',
|
||||
dataIndex: 'value_type',
|
||||
key: 'value_type',
|
||||
render: (type: string) => <Tag>{type}</Tag>,
|
||||
},
|
||||
{
|
||||
title: 'Descripcion',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Secreto',
|
||||
dataIndex: 'is_secret',
|
||||
key: 'is_secret',
|
||||
render: (isSecret: boolean) =>
|
||||
isSecret ? <Tag color="red">Si</Tag> : <Tag>No</Tag>,
|
||||
},
|
||||
{
|
||||
title: 'Acciones',
|
||||
key: 'actions',
|
||||
render: (_: unknown, record: GlobalVariable) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenEdit(record)}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="Eliminar esta variable?"
|
||||
onConfirm={() => deleteMutation.mutate(record.key)}
|
||||
okText="Si"
|
||||
cancelText="No"
|
||||
>
|
||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 24 }}>
|
||||
<Title level={4} style={{ margin: 0 }}>Variables Globales</Title>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleOpenCreate}>
|
||||
Nueva Variable
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={variables || []}
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
pagination={false}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingVariable ? 'Editar Variable' : 'Nueva Variable'}
|
||||
open={isModalOpen}
|
||||
onCancel={handleCloseModal}
|
||||
onOk={handleSubmit}
|
||||
okText={editingVariable ? 'Guardar' : 'Crear'}
|
||||
confirmLoading={createMutation.isPending || updateMutation.isPending}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="key"
|
||||
label="Clave"
|
||||
rules={[
|
||||
{ required: true, message: 'Ingrese la clave' },
|
||||
{ pattern: /^[A-Z_][A-Z0-9_]*$/, message: 'Use MAYUSCULAS_CON_GUIONES' },
|
||||
]}
|
||||
>
|
||||
<Input disabled={!!editingVariable} placeholder="API_KEY" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="value"
|
||||
label={editingVariable?.is_secret ? 'Nuevo Valor (dejar vacio para mantener)' : 'Valor'}
|
||||
rules={editingVariable ? [] : [{ required: true, message: 'Ingrese el valor' }]}
|
||||
>
|
||||
<TextArea rows={2} placeholder="Valor de la variable" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="value_type" label="Tipo">
|
||||
<Select>
|
||||
<Select.Option value="string">String</Select.Option>
|
||||
<Select.Option value="number">Number</Select.Option>
|
||||
<Select.Option value="boolean">Boolean</Select.Option>
|
||||
<Select.Option value="json">JSON</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="Descripcion">
|
||||
<Input placeholder="Descripcion opcional" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="is_secret" valuePropName="checked">
|
||||
<Checkbox>Es secreto (no mostrar valor)</Checkbox>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user