plugin-postgresql-connector
Version:
NocoBase plugin for connecting to external PostgreSQL databases
235 lines • 20.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryBuilder = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const antd_1 = require("antd");
const icons_1 = require("@ant-design/icons");
const react_ace_1 = __importDefault(require("react-ace"));
require("ace-builds/src-noconflict/mode-sql");
require("ace-builds/src-noconflict/theme-github");
require("ace-builds/src-noconflict/theme-monokai");
require("ace-builds/src-noconflict/ext-language_tools");
const client_1 = require("@nocobase/client");
const { Option } = antd_1.Select;
const { Title, Text } = antd_1.Typography;
const { TabPane } = antd_1.Tabs;
const { Search } = antd_1.Input;
const QueryBuilder = ({ connectionId: propConnectionId }) => {
const [selectedConnection, setSelectedConnection] = (0, react_1.useState)(propConnectionId || '');
const [query, setQuery] = (0, react_1.useState)('-- Chọn connection và viết SQL query của bạn\nSELECT * FROM ');
const [queryResult, setQueryResult] = (0, react_1.useState)(null);
const [schemaInfo, setSchemaInfo] = (0, react_1.useState)(null);
const [selectedTable, setSelectedTable] = (0, react_1.useState)('');
const [expandedKeys, setExpandedKeys] = (0, react_1.useState)(['tables', 'views', 'functions']);
const [searchValue, setSearchValue] = (0, react_1.useState)('');
const [queryParams, setQueryParams] = (0, react_1.useState)([]);
const [savedQueries, setSavedQueries] = (0, react_1.useState)([]);
const [saveModalVisible, setSaveModalVisible] = (0, react_1.useState)(false);
const [querySettings, setQuerySettings] = (0, react_1.useState)({
theme: 'github',
fontSize: 14,
maxRows: 1000,
timeout: 30000,
});
// Fetch connections
const { data: connections, loading: connectionsLoading } = (0, client_1.useRequest)({
url: '/postgresql-connections',
});
// Fetch schema info
const { run: fetchSchema, loading: schemaLoading } = (0, client_1.useRequest)((connectionId) => ({
url: `/postgresql-schema/${connectionId}/overview`,
}), {
manual: true,
onSuccess: (data) => {
setSchemaInfo(data);
},
onError: (error) => {
antd_1.message.error(`Không thể tải schema: ${error.message}`);
},
});
// Execute query
const { run: executeQuery, loading: executing } = (0, client_1.useRequest)((data) => ({
url: '/postgresql-query/execute',
method: 'POST',
data,
}), {
manual: true,
onSuccess: (data) => {
setQueryResult(data);
antd_1.message.success(`Query thực thi thành công! (${data.data.executionTime}ms)`);
},
onError: (error) => {
antd_1.message.error(`Query thất bại: ${error.message}`);
setQueryResult({ error: error.message });
},
});
// Save query
const { run: saveQuery, loading: saving } = (0, client_1.useRequest)((data) => ({
url: '/postgresql-saved-queries',
method: 'POST',
data,
}), {
manual: true,
onSuccess: () => {
antd_1.message.success('Query đã được lưu thành công!');
setSaveModalVisible(false);
fetchSavedQueries();
},
onError: (error) => {
antd_1.message.error(`Lưu query thất bại: ${error.message}`);
},
});
// Fetch saved queries
const { run: fetchSavedQueries } = (0, client_1.useRequest)(() => ({
url: '/postgresql-saved-queries',
params: { connectionId: selectedConnection },
}), {
manual: true,
onSuccess: (data) => {
setSavedQueries(data.data || []);
},
});
// Fetch table data
const { run: fetchTableData } = (0, client_1.useRequest)((tableName) => ({
url: '/postgresql-query/getTableData',
params: {
connectionId: selectedConnection,
tableName,
limit: 100
},
}), {
manual: true,
onSuccess: (data) => {
setQueryResult(data);
setQuery(`SELECT * FROM ${tableName} LIMIT 100;`);
},
});
(0, react_1.useEffect)(() => {
if (selectedConnection) {
fetchSchema(selectedConnection);
fetchSavedQueries();
}
}, [selectedConnection]);
const handleExecuteQuery = () => {
if (!selectedConnection || !query.trim()) {
antd_1.message.warning('Vui lòng chọn connection và nhập query');
return;
}
executeQuery({
connectionId: selectedConnection,
query: query.trim(),
parameters: queryParams,
options: {
maxRows: querySettings.maxRows,
timeout: querySettings.timeout,
formatQuery: true,
includeMetadata: true,
},
});
};
const handleSaveQuery = (values) => {
saveQuery({
connectionId: selectedConnection,
name: values.name,
query: query.trim(),
queryType: detectQueryType(query),
description: values.description,
category: values.category || 'general',
});
};
const detectQueryType = (query) => {
const upperQuery = query.toUpperCase().trim();
if (upperQuery.startsWith('SELECT'))
return 'SELECT';
if (upperQuery.startsWith('INSERT'))
return 'INSERT';
if (upperQuery.startsWith('UPDATE'))
return 'UPDATE';
if (upperQuery.startsWith('DELETE'))
return 'DELETE';
return 'SELECT';
};
const buildSchemaTree = () => {
if (!schemaInfo?.databaseInfo)
return [];
const treeData = [
{
title: ((0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)(icons_1.DatabaseOutlined, { style: { marginRight: 8 } }), (0, jsx_runtime_1.jsx)("strong", { children: schemaInfo.databaseInfo.database_name }), (0, jsx_runtime_1.jsxs)(antd_1.Tag, { color: "blue", style: { marginLeft: 8 }, children: [schemaInfo.statistics?.total_tables || 0, " tables"] })] })),
key: 'database',
children: [
{
title: ((0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)(icons_1.TableOutlined, { style: { marginRight: 8 } }), "Tables (", schemaInfo.recentTables?.length || 0, ")"] })),
key: 'tables',
children: schemaInfo.recentTables?.map((table) => ({
title: ((0, jsx_runtime_1.jsxs)("span", { style: { cursor: 'pointer' }, onClick: () => {
setQuery(`SELECT * FROM ${table.table_name} LIMIT 10;`);
fetchTableData(table.table_name);
}, children: [(0, jsx_runtime_1.jsx)(icons_1.TableOutlined, { style: { marginRight: 8, color: '#1890ff' } }), table.table_name, table.row_count && ((0, jsx_runtime_1.jsxs)(antd_1.Tag, { size: "small", style: { marginLeft: 8 }, children: [table.row_count, " rows"] }))] })),
key: `table-${table.table_name}`,
isLeaf: true,
})) || [],
},
{
title: ((0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)(icons_1.DatabaseOutlined, { style: { marginRight: 8 } }), "Views (", schemaInfo.recentViews?.length || 0, ")"] })),
key: 'views',
children: schemaInfo.recentViews?.map((view) => ({
title: ((0, jsx_runtime_1.jsxs)("span", { style: { cursor: 'pointer' }, onClick: () => setQuery(`SELECT * FROM ${view.view_name} LIMIT 10;`), children: [(0, jsx_runtime_1.jsx)(icons_1.DatabaseOutlined, { style: { marginRight: 8, color: '#52c41a' } }), view.view_name] })),
key: `view-${view.view_name}`,
isLeaf: true,
})) || [],
},
{
title: ((0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)(icons_1.FunctionOutlined, { style: { marginRight: 8 } }), "Functions (", schemaInfo.recentFunctions?.length || 0, ")"] })),
key: 'functions',
children: schemaInfo.recentFunctions?.map((func) => ({
title: ((0, jsx_runtime_1.jsxs)("span", { style: { cursor: 'pointer' }, onClick: () => setQuery(`SELECT ${func.function_name}();`), children: [(0, jsx_runtime_1.jsx)(icons_1.FunctionOutlined, { style: { marginRight: 8, color: '#fa8c16' } }), func.function_name, (0, jsx_runtime_1.jsx)(antd_1.Tag, { size: "small", color: "orange", style: { marginLeft: 8 }, children: func.routine_type })] })),
key: `function-${func.function_name}`,
isLeaf: true,
})) || [],
},
],
},
];
return treeData;
};
const renderQueryResult = () => {
if (!queryResult)
return null;
if (queryResult.error) {
return ((0, jsx_runtime_1.jsx)("div", { style: { padding: 16, textAlign: 'center' }, children: (0, jsx_runtime_1.jsx)(Text, { type: "danger", children: queryResult.error }) }));
}
const { data } = queryResult;
if (!data.rows || data.rows.length === 0) {
return ((0, jsx_runtime_1.jsx)("div", { style: { padding: 16, textAlign: 'center' }, children: (0, jsx_runtime_1.jsx)(Text, { type: "secondary", children: "Kh\u00F4ng c\u00F3 d\u1EEF li\u1EC7u" }) }));
}
const columns = data.fields?.map((field) => ({
title: ((0, jsx_runtime_1.jsx)(antd_1.Tooltip, { title: `Type: ${field.dataTypeName || field.dataTypeID}`, children: (0, jsx_runtime_1.jsx)("span", { children: field.name }) })),
dataIndex: field.name,
key: field.name,
ellipsis: { showTitle: false },
render: (text) => ((0, jsx_runtime_1.jsx)(antd_1.Tooltip, { title: text, children: (0, jsx_runtime_1.jsx)("span", { children: text !== null ? String(text) : (0, jsx_runtime_1.jsx)(Text, { type: "secondary", children: "NULL" }) }) })),
}));
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [(0, jsx_runtime_1.jsxs)(antd_1.Space, { children: [(0, jsx_runtime_1.jsx)(antd_1.Statistic, { title: "Rows", value: data.rowCount }), (0, jsx_runtime_1.jsx)(antd_1.Statistic, { title: "Execution Time", value: data.executionTime, suffix: "ms" }), (0, jsx_runtime_1.jsx)(antd_1.Statistic, { title: "Columns", value: data.fields?.length || 0 })] }), (0, jsx_runtime_1.jsx)(antd_1.Button, { icon: (0, jsx_runtime_1.jsx)(icons_1.DownloadOutlined, {}), size: "small", children: "Export CSV" })] }), (0, jsx_runtime_1.jsx)(antd_1.Table, { columns: columns, dataSource: data.rows, rowKey: (record, index) => index, scroll: { x: true, y: 400 }, pagination: {
pageSize: 50,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `Tổng ${total} rows`,
}, size: "small" })] }));
};
const renderSavedQueries = () => ((0, jsx_runtime_1.jsxs)("div", { style: { padding: 16 }, children: [(0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: 16, display: 'flex', justifyContent: 'space-between' }, children: [(0, jsx_runtime_1.jsx)(Title, { level: 5, children: "Saved Queries" }), (0, jsx_runtime_1.jsx)(antd_1.Button, { size: "small", onClick: fetchSavedQueries, children: "Refresh" })] }), savedQueries.map((savedQuery) => ((0, jsx_runtime_1.jsx)(antd_1.Card, { size: "small", style: { marginBottom: 8, cursor: 'pointer' }, onClick: () => setQuery(savedQuery.query), hoverable: true, children: (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(Text, { strong: true, children: savedQuery.name }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)(Text, { type: "secondary", style: { fontSize: 12 }, children: savedQuery.description })] }), (0, jsx_runtime_1.jsx)(antd_1.Tag, { color: "blue", children: savedQuery.queryType })] }) }, savedQuery.id)))] }));
return ((0, jsx_runtime_1.jsxs)("div", { style: { padding: 24, height: '100vh', display: 'flex', flexDirection: 'column' }, children: [(0, jsx_runtime_1.jsx)(antd_1.Card, { style: { marginBottom: 16 }, children: (0, jsx_runtime_1.jsxs)(antd_1.Row, { gutter: [16, 16], align: "middle", children: [(0, jsx_runtime_1.jsx)(antd_1.Col, { span: 8, children: (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(Text, { strong: true, children: "Connection:" }), (0, jsx_runtime_1.jsx)(antd_1.Select, { placeholder: "Ch\u1ECDn k\u1EBFt n\u1ED1i PostgreSQL", style: { width: '100%', marginTop: 8 }, loading: connectionsLoading, value: selectedConnection, onChange: setSelectedConnection, size: "large", children: connections?.data?.map((conn) => ((0, jsx_runtime_1.jsx)(Option, { value: conn.id, children: (0, jsx_runtime_1.jsxs)(antd_1.Space, { children: [(0, jsx_runtime_1.jsx)(icons_1.DatabaseOutlined, {}), conn.name, " (", conn.host, ":", conn.port, ")"] }) }, conn.id))) })] }) }), (0, jsx_runtime_1.jsx)(antd_1.Col, { span: 8, children: schemaInfo && ((0, jsx_runtime_1.jsxs)(antd_1.Space, { children: [(0, jsx_runtime_1.jsx)(antd_1.Statistic, { title: "Tables", value: schemaInfo.statistics?.total_tables || 0 }), (0, jsx_runtime_1.jsx)(antd_1.Statistic, { title: "Views", value: schemaInfo.statistics?.total_views || 0 }), (0, jsx_runtime_1.jsx)(antd_1.Statistic, { title: "Functions", value: schemaInfo.statistics?.total_functions || 0 })] })) }), (0, jsx_runtime_1.jsx)(antd_1.Col, { span: 8, style: { textAlign: 'right' }, children: (0, jsx_runtime_1.jsxs)(antd_1.Space, { children: [(0, jsx_runtime_1.jsx)(antd_1.Button, { icon: (0, jsx_runtime_1.jsx)(icons_1.SettingOutlined, {}), onClick: () => antd_1.message.info('Settings panel coming soon'), children: "Settings" }), (0, jsx_runtime_1.jsx)(antd_1.Button, { icon: (0, jsx_runtime_1.jsx)(icons_1.InfoCircleOutlined, {}), onClick: () => antd_1.message.info('Database info panel coming soon'), children: "DB Info" })] }) })] }) }), (0, jsx_runtime_1.jsxs)("div", { style: { flex: 1, display: 'flex', gap: 16 }, children: [(0, jsx_runtime_1.jsx)("div", { style: { width: 320, display: 'flex', flexDirection: 'column' }, children: (0, jsx_runtime_1.jsx)(antd_1.Card, { style: { flex: 1 }, bodyStyle: { padding: 0 }, children: (0, jsx_runtime_1.jsxs)(antd_1.Tabs, { defaultActiveKey: "schema", size: "small", children: [(0, jsx_runtime_1.jsx)(TabPane, { tab: (0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)(icons_1.DatabaseOutlined, {}), "Schema"] }), children: (0, jsx_runtime_1.jsxs)("div", { style: { padding: 16 }, children: [(0, jsx_runtime_1.jsx)(Search, { placeholder: "T\u00ECm ki\u1EBFm tables, views...", value: searchValue, onChange: (e) => setSearchValue(e.target.value), style: { marginBottom: 16 } }), schemaLoading ? ((0, jsx_runtime_1.jsx)("div", { style: { textAlign: 'center', padding: 20 }, children: (0, jsx_runtime_1.jsx)(Text, { type: "secondary", children: "Loading schema..." }) })) : ((0, jsx_runtime_1.jsx)(antd_1.Tree, { showIcon: true, defaultExpandAll: true, expandedKeys: expandedKeys, onExpand: setExpandedKeys, treeData: buildSchemaTree() }))] }) }, "schema"), (0, jsx_runtime_1.jsx)(TabPane, { tab: (0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)(icons_1.HistoryOutlined, {}), "Saved"] }), children: renderSavedQueries() }, "saved")] }) }) }), (0, jsx_runtime_1.jsxs)("div", { style: { flex: 1, display: 'flex', flexDirection: 'column' }, children: [(0, jsx_runtime_1.jsxs)(antd_1.Card, { style: { marginBottom: 16 }, children: [(0, jsx_runtime_1.jsx)("div", { style: { marginBottom: 16 }, children: (0, jsx_runtime_1.jsxs)(antd_1.Space, { children: [(0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", icon: (0, jsx_runtime_1.jsx)(icons_1.PlayCircleOutlined, {}), onClick: handleExecuteQuery, loading: executing, disabled: !selectedConnection, size: "large", children: executing ? 'Đang thực thi...' : 'Thực thi Query' }), (0, jsx_runtime_1.jsx)(antd_1.Button, { icon: (0, jsx_runtime_1.jsx)(icons_1.SaveOutlined, {}), onClick: () => setSaveModalVisible(true), disabled: !query.trim() || !selectedConnection, children: "L\u01B0u Query" }), (0, jsx_runtime_1.jsx)(antd_1.Divider, { type: "vertical" }), (0, jsx_runtime_1.jsx)(Text, { type: "secondary", children: "Theme:" }), (0, jsx_runtime_1.jsxs)(antd_1.Select, { value: querySettings.theme, onChange: (theme) => setQuerySettings(prev => ({ ...prev, theme })), style: { width: 100 }, size: "small", children: [(0, jsx_runtime_1.jsx)(Option, { value: "github", children: "Light" }), (0, jsx_runtime_1.jsx)(Option, { value: "monokai", children: "Dark" })] })] }) }), (0, jsx_runtime_1.jsx)(react_ace_1.default, { mode: "sql", theme: querySettings.theme, value: query, onChange: setQuery, width: "100%", height: "300px", fontSize: querySettings.fontSize, showPrintMargin: true, showGutter: true, highlightActiveLine: true, setOptions: {
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
showLineNumbers: true,
tabSize: 2,
wrap: false,
}, style: { border: '1px solid #d9d9d9', borderRadius: 6 } })] }), (0, jsx_runtime_1.jsx)(antd_1.Card, { title: "K\u1EBFt qu\u1EA3 Query", style: { flex: 1 }, bodyStyle: { padding: 0 }, children: renderQueryResult() })] })] }), (0, jsx_runtime_1.jsx)(antd_1.Modal, { title: "L\u01B0u Query", open: saveModalVisible, onCancel: () => setSaveModalVisible(false), footer: null, children: (0, jsx_runtime_1.jsxs)(antd_1.Form, { onFinish: handleSaveQuery, layout: "vertical", children: [(0, jsx_runtime_1.jsx)(antd_1.Form.Item, { name: "name", label: "T\u00EAn Query", rules: [{ required: true, message: 'Vui lòng nhập tên query' }], children: (0, jsx_runtime_1.jsx)(antd_1.Input, { placeholder: "V\u00ED d\u1EE5: Get active users" }) }), (0, jsx_runtime_1.jsx)(antd_1.Form.Item, { name: "description", label: "M\u00F4 t\u1EA3", children: (0, jsx_runtime_1.jsx)(antd_1.Input.TextArea, { placeholder: "M\u00F4 t\u1EA3 ng\u1EAFn v\u1EC1 query n\u00E0y...", rows: 3 }) }), (0, jsx_runtime_1.jsx)(antd_1.Form.Item, { name: "category", label: "Category", children: (0, jsx_runtime_1.jsxs)(antd_1.Select, { placeholder: "Ch\u1ECDn category", children: [(0, jsx_runtime_1.jsx)(Option, { value: "general", children: "General" }), (0, jsx_runtime_1.jsx)(Option, { value: "reports", children: "Reports" }), (0, jsx_runtime_1.jsx)(Option, { value: "maintenance", children: "Maintenance" }), (0, jsx_runtime_1.jsx)(Option, { value: "analysis", children: "Analysis" })] }) }), (0, jsx_runtime_1.jsx)(antd_1.Form.Item, { children: (0, jsx_runtime_1.jsxs)(antd_1.Space, { children: [(0, jsx_runtime_1.jsx)(antd_1.Button, { onClick: () => setSaveModalVisible(false), children: "H\u1EE7y" }), (0, jsx_runtime_1.jsx)(antd_1.Button, { type: "primary", htmlType: "submit", loading: saving, children: "L\u01B0u Query" })] }) })] }) })] }));
};
exports.QueryBuilder = QueryBuilder;
exports.default = exports.QueryBuilder;
//# sourceMappingURL=QueryBuilder.js.map