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