UNPKG

plugin-postgresql-connector

Version:

NocoBase plugin for connecting to external PostgreSQL databases

235 lines 20.3 kB
"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 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 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