diff --git a/api/src/controllers/tables.js b/api/src/controllers/tables.js index ebcb169a5..69163588f 100644 --- a/api/src/controllers/tables.js +++ b/api/src/controllers/tables.js @@ -8,4 +8,11 @@ module.exports = { const res = await databaseConnections.sendRequest(opened, { msgtype: 'tableData', schemaName, pureName }); return res; }, + + tableInfo_meta: 'get', + async tableInfo({ conid, database, schemaName, pureName }) { + const opened = await databaseConnections.ensureOpened(conid, database); + const table = opened.structure.tables.find(x => x.pureName == pureName && x.schemaName == schemaName); + return table; + }, }; diff --git a/api/src/engines/mssql/MsSqlAnalyser.js b/api/src/engines/mssql/MsSqlAnalyser.js index 941a89d9c..852a06bde 100644 --- a/api/src/engines/mssql/MsSqlAnalyser.js +++ b/api/src/engines/mssql/MsSqlAnalyser.js @@ -1,5 +1,6 @@ const fs = require('fs-extra'); const path = require('path'); +const _ = require('lodash'); const DatabaseAnalayser = require('../default/DatabaseAnalyser'); @@ -27,13 +28,18 @@ class MsSqlAnalyser extends DatabaseAnalayser { } async runAnalysis() { const tables = await this.driver.query(this.pool, await this.createQuery('tables.sql')); - // for (const table of tables) { - // table.name = { - // schema: table.schemaName, - // name: table.tableName, - // }; - // } - this.result.tables = tables.rows; + const columns = await this.driver.query(this.pool, await this.createQuery('columns.sql')); + + this.result.tables = tables.rows.map(table => ({ + ...table, + columns: columns.rows + .filter(col => col.objectId == table.objectId) + .map(({ isNullable, isIdentity, ...col }) => ({ + ...col, + notNull: isNullable != 'True', + autoIncrement: isIdentity == 'True', + })), + })); } } diff --git a/api/src/engines/mssql/columns.sql b/api/src/engines/mssql/columns.sql new file mode 100644 index 000000000..34ca9050c --- /dev/null +++ b/api/src/engines/mssql/columns.sql @@ -0,0 +1,13 @@ +select c.name as columnName, t.name as dataType, c.object_id as objectId, c.is_identity as isIdentity, + c.max_length as maxLength, c.precision, c.scale, c.is_nullable as isNullable, + d.definition as defaultValue, d.name as defaultConstraint, + m.definition as computedExpression, m.is_persisted as isPersisted, c.column_id as columnId, + -- TODO only if version >= 2008 + c.is_sparse as isSparse +from sys.columns c +inner join sys.types t on c.system_type_id = t.system_type_id and c.user_type_id = t.user_type_id +inner join sys.objects o on c.object_id = o.object_id +left join sys.default_constraints d on c.default_object_id = d.object_id +left join sys.computed_columns m on m.object_id = c.object_id and m.column_id = c.column_id +where o.type = 'U' and o.object_id =[OBJECT_ID_CONDITION] +order by c.column_id diff --git a/api/src/engines/mssql/tables.sql b/api/src/engines/mssql/tables.sql index 4c96a159c..a90248ad8 100644 --- a/api/src/engines/mssql/tables.sql +++ b/api/src/engines/mssql/tables.sql @@ -1,5 +1,5 @@ select - o.name as pureName, s.name as schemaName, o.object_id, + o.name as pureName, s.name as schemaName, o.object_id as objectId, o.create_date, o.modify_date from sys.tables o inner join sys.schemas s on o.schema_id = s.schema_id diff --git a/lib/src/dbinfo.ts b/lib/src/dbinfo.ts new file mode 100644 index 000000000..368df8a61 --- /dev/null +++ b/lib/src/dbinfo.ts @@ -0,0 +1,29 @@ +import { ChildProcess } from "child_process"; + +export interface NamedObjectInfo { + pureName: string; + schemaName: string; +} + +export interface ColumnInfo { + columnName: string; + notNull: boolean; + autoIncrement: boolean; + dataType: string; + precision: number; + scale: number; + length: number; + computedExpression: string; + isPersisted: boolean; + isSparse: boolean; + defaultValue: string; + defaultConstraint: string; +} + +export interface TableInfo extends NamedObjectInfo { + columns: ColumnInfo[]; +} + +export interface DatabaseInfo { + tables: TableInfo[]; +} diff --git a/lib/src/engines.ts b/lib/src/engines.ts new file mode 100644 index 000000000..cd749125e --- /dev/null +++ b/lib/src/engines.ts @@ -0,0 +1,11 @@ +import { QueryResult } from "./query"; + +export interface EngineDriver { + connect({ server, port, user, password }); + query(pool, sql: string): Promise; + getVersion(pool): Promise; + listDatabases(pool): Promise<{ name: string }[]>; + analyseFull(pool): Promise; + analyseIncremental(pool): Promise; + } + \ No newline at end of file diff --git a/lib/src/index.ts b/lib/src/index.ts index 9d7dcdd2b..a87b47ff1 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -1,28 +1,5 @@ import { ChildProcess } from "child_process"; - -export interface QueryResult { - rows: any[]; -} - -export interface EngineDriver { - connect({ server, port, user, password }); - query(pool, sql: string): Promise; - getVersion(pool): Promise; - listDatabases(pool): Promise<{ name: string }[]>; - analyseFull(pool): Promise; - analyseIncremental(pool): Promise; -} - -export interface NamedObjectInfo { - pureName: string; - schemaName: string; -} - -export interface TableInfo extends NamedObjectInfo {} - -export interface DatabaseInfo { - tables: TableInfo[]; -} +import { DatabaseInfo } from "./dbinfo"; export interface OpenedDatabaseConnection { conid: string; @@ -31,6 +8,6 @@ export interface OpenedDatabaseConnection { subprocess: ChildProcess; } -export function sum(a: number, b: number) { - return a + b; -} +export * from "./engines"; +export * from "./dbinfo"; +export * from "./query"; diff --git a/lib/src/query.ts b/lib/src/query.ts new file mode 100644 index 000000000..587146aeb --- /dev/null +++ b/lib/src/query.ts @@ -0,0 +1,5 @@ +import { ChildProcess } from "child_process"; + +export interface QueryResult { + rows: any[]; +} diff --git a/web/package.json b/web/package.json index 3ecb17902..dcc4e4997 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,7 @@ "resize-observer-polyfill": "^1.5.1", "socket.io-client": "^2.3.0", "styled-components": "^4.4.1", + "@dbgate/lib": "file:../lib", "uuid": "^3.4.0" }, "scripts": { diff --git a/web/src/appobj/AppObjects.js b/web/src/appobj/AppObjects.js index 33ed14a38..bb51a805c 100644 --- a/web/src/appobj/AppObjects.js +++ b/web/src/appobj/AppObjects.js @@ -17,11 +17,13 @@ const IconWrap = styled.span` `; export function AppObjectCore({ title, Icon, Menu, data, makeAppObj, onClick }) { + const setOpenedTabs = useSetOpenedTabs(); + const handleContextMenu = event => { if (!Menu) return; event.preventDefault(); - showMenu(event.pageX, event.pageY, ); + showMenu(event.pageX, event.pageY, ); }; return ( diff --git a/web/src/appobj/columnAppObject.js b/web/src/appobj/columnAppObject.js new file mode 100644 index 000000000..0bc956a66 --- /dev/null +++ b/web/src/appobj/columnAppObject.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { TableIcon } from '../icons'; +import { DropDownMenuItem } from '../modals/DropDownMenu'; +import showModal from '../modals/showModal'; +import ConnectionModal from '../modals/ConnectionModal'; +import axios from '../utility/axios'; +import { openNewTab } from '../utility/common'; +import { useSetOpenedTabs } from '../utility/globalState'; + +/** @param columnProps {import('@dbgate/lib').ColumnInfo} */ +export default function columnAppObject(columnProps, { setOpenedTabs }) { + const title = columnProps.columnName; + const key = title; + const Icon = TableIcon; + + return { title, key, Icon }; +} diff --git a/web/src/appobj/tableAppObject.js b/web/src/appobj/tableAppObject.js index c74869c40..8e1667f20 100644 --- a/web/src/appobj/tableAppObject.js +++ b/web/src/appobj/tableAppObject.js @@ -5,18 +5,33 @@ import showModal from '../modals/showModal'; import ConnectionModal from '../modals/ConnectionModal'; import axios from '../utility/axios'; import { openNewTab } from '../utility/common'; +import { useSetOpenedTabs } from '../utility/globalState'; -function Menu({ data, makeAppObj }) { - const handleEdit = () => { - showModal(modalState => ); +function openTableDetail(setOpenedTabs, tabComponent, { schemaName, pureName, conid, database }) { + openNewTab(setOpenedTabs, { + title: pureName, + icon: 'table2.svg', + tabComponent, + props: { + schemaName, + pureName, + conid, + database, + }, + }); +} + +function Menu({ data, makeAppObj, setOpenedTabs }) { + const handleOpenData = () => { + openTableDetail(setOpenedTabs, 'TableDataTab', data); }; - const handleDelete = () => { - axios.post('connections/delete', data); + const handleOpenStructure = () => { + openTableDetail(setOpenedTabs, 'TableStructureTab', data); }; return ( <> - Edit - Delete + Open data + Open structure ); } @@ -26,16 +41,11 @@ export default function tableAppObject({ conid, database, pureName, schemaName } const key = title; const Icon = TableIcon; const onClick = ({ schemaName, pureName }) => { - openNewTab(setOpenedTabs, { - title: pureName, - icon: 'table2.svg', - tabComponent: 'TableDataTab', - props: { - schemaName, - pureName, - conid, - database, - }, + openTableDetail(setOpenedTabs, 'TableDataTab', { + schemaName, + pureName, + conid, + database, }); }; diff --git a/web/src/index.css b/web/src/index.css index ebce15bce..809b4ae2a 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,7 +1,10 @@ body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + font-family: -apple-system,BlinkMacSystemFont,Segoe WPC,Segoe UI,HelveticaNeue-Light,Ubuntu,Droid Sans,sans-serif; + font-size: 14px; + /* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + sans-serif; + */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/web/src/tabs/TableStructureTab.js b/web/src/tabs/TableStructureTab.js new file mode 100644 index 000000000..be2c7817e --- /dev/null +++ b/web/src/tabs/TableStructureTab.js @@ -0,0 +1,78 @@ +import React from 'react'; +import useFetch from '../utility/useFetch'; +import styled from 'styled-components'; +import theme from '../theme'; +import ObjectListControl from '../utility/ObjectListControl'; +import { TableColumn } from '../utility/TableControl'; + +const WhitePage = styled.div` + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background-color: white; +`; + +export default function TableStructureTab({ conid, database, schemaName, pureName }) { + /** @type {import('@dbgate/lib').TableInfo} */ + const tableInfo = useFetch({ + url: 'tables/table-info', + params: { conid, database, schemaName, pureName }, + }); + if (!tableInfo) return null; + return ( + + ({ ...x, ordinal: index + 1 }))} + title="Columns" + > + (row.notNull ? 'YES' : 'NO')} + /> + + + (row.isSparse ? 'YES' : 'NO')} + /> + + (row.isPersisted ? 'YES' : 'NO')} + /> + {/* {_.includes(dbCaps.columnListOptionalColumns, 'referencedTableNamesFormatted') && ( + + )} + ( + + this.deleteColumn(row)} + > + Delete + {' '} + |{' '} + this.editColumn(row)} + > + Edit + + + )} + /> */} + + + ); +} diff --git a/web/src/tabs/index.js b/web/src/tabs/index.js index 5c41b3a1a..d26516093 100644 --- a/web/src/tabs/index.js +++ b/web/src/tabs/index.js @@ -1,5 +1,7 @@ import TableDataTab from './TableDataTab'; +import TableStructureTab from './TableStructureTab'; export default { TableDataTab, + TableStructureTab, }; diff --git a/web/src/utility/ObjectListControl.js b/web/src/utility/ObjectListControl.js new file mode 100644 index 000000000..fc1de7e59 --- /dev/null +++ b/web/src/utility/ObjectListControl.js @@ -0,0 +1,41 @@ +import React from 'react'; +import useFetch from '../utility/useFetch'; +import styled from 'styled-components'; +import theme from '../theme'; +import TableControl from './TableControl'; + +const ObjectListWrapper = styled.div` + margin-bottom: 20px; +`; + +const ObjectListHeader = styled.div` + background-color: #ebedef; + padding: 5px; +`; + +const ObjectListHeaderTitle = styled.span` + font-weight: bold; + margin-left: 5px; +`; + +const ObjectListBody = styled.div` + margin: 20px; + // margin-left: 20px; + // margin-right: 20px; + // margin-top: 3px; +`; + +export default function ObjectListControl({ collection = [], title, showIfEmpty = false, children }) { + if (collection.length == 0 && !showIfEmpty) return null; + + return ( + + + {title} + + + {children} + + + ); +} diff --git a/web/src/utility/TableControl.js b/web/src/utility/TableControl.js new file mode 100644 index 000000000..53b6d6786 --- /dev/null +++ b/web/src/utility/TableControl.js @@ -0,0 +1,59 @@ +import React from 'react'; +import useFetch from '../utility/useFetch'; +import styled from 'styled-components'; +import theme from '../theme'; + +const Table = styled.table` + border-collapse: collapse; + width: 100%; +`; +const TableHead = styled.thead``; +const TableBody = styled.tbody``; +const TableHeaderRow = styled.tr``; +const TableBodyRow = styled.tr``; +const TableHeaderCell = styled.td` + border: 1px solid #e8eef4; + background-color: #e8eef4; + padding: 5px; +`; +const TableBodyCell = styled.td` + border: 1px solid #e8eef4; + padding: 5px; +`; + +export function TableColumn({ fieldName, header, sortable, formatter = undefined }) { + return <>; +} + +function format(row, col) { + const { formatter, fieldName } = col; + if (formatter) return formatter(row); + return row[fieldName]; +} + +export default function TableControl({ rows = [], children }) { + const columns = (children instanceof Array ? children : [children]) + .filter(child => child != null) + .map(child => child.props); + + return ( + + + + {columns.map(x => ( + {x.header} + ))} + + + + {rows.map((row, index) => ( + + {columns.map(col => ( + {format(row, col)} + ))} + + ))} + +
+ ); +} diff --git a/web/tsconfig.json b/web/tsconfig.json index 5abb91718..efee30703 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -15,12 +15,12 @@ "lib": [ "dom", "dom.iterable", - "esnext" + "esnext", ], "strict": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "isolatedModules": true + "isolatedModules": true, }, "include": [ "src"