feat(frontend): add GlobalVariables management page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude AI
2026-01-29 11:16:20 +00:00
parent bcb09204ae
commit 20e5c2d13b

View 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>
);
}