Skip to content

Commit 36ec7e8

Browse files
committed
feat: add data table tab
1 parent e29dc9c commit 36ec7e8

File tree

13 files changed

+269
-111
lines changed

13 files changed

+269
-111
lines changed

src/drivers/base/NotImplementCommonInterface.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import SQLCommonInterface from './SQLCommonInterface';
33

44
export default class NotImplementCommonInterface extends SQLCommonInterface {
55
async getSchema(): Promise<DatabaseSchemas> {
6-
throw 'Not implemented';
6+
throw new Error('Not implemented');
77
}
88

99
async getTableSchema(): Promise<TableDefinitionSchema> {
10-
throw 'Not implemented';
10+
throw new Error('Not implemented');
1111
}
1212

1313
async switchDatabase(): Promise<boolean> {
14-
throw 'Not implemented';
14+
throw new Error('Not implemented');
1515
}
1616

1717
async getVersion(): Promise<string> {
18-
throw 'Not implemented';
18+
throw new Error('Not implemented');
1919
}
2020
}

src/libs/QueryBuilder.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import SqlString from 'sqlstring';
33
interface QueryRaw {
44
__typename: 'query_raw';
55
raw: string;
6-
binding: unknown[];
6+
binding?: unknown[];
77
}
88

99
interface QueryWhere {
@@ -22,8 +22,9 @@ interface QueryStates {
2222
insert?: Record<string, unknown>;
2323
update?: Record<string, unknown>;
2424
where: QueryWhere[];
25-
select: string[];
25+
select: (string | QueryRaw)[];
2626
limit?: number;
27+
offset?: number;
2728
}
2829

2930
abstract class QueryDialect {
@@ -55,6 +56,21 @@ export class QueryBuilder {
5556
}
5657
}
5758

59+
protected escapeIdentifier(id: string | QueryRaw) {
60+
if (typeof id === 'string') {
61+
return this.dialect.escapeIdentifier(id);
62+
}
63+
64+
return id.raw;
65+
}
66+
67+
raw(str: string): QueryRaw {
68+
return {
69+
__typename: 'query_raw',
70+
raw: str,
71+
};
72+
}
73+
5874
escapeId(name: string) {
5975
return this.dialect.escapeIdentifier(name);
6076
}
@@ -98,7 +114,7 @@ export class QueryBuilder {
98114
return this;
99115
}
100116

101-
select(...columns: string[]) {
117+
select(...columns: (string | QueryRaw)[]) {
102118
this.states.select = this.states.select.concat(columns);
103119
return this;
104120
}
@@ -108,6 +124,11 @@ export class QueryBuilder {
108124
return this;
109125
}
110126

127+
offset(n: number) {
128+
this.states.offset = n;
129+
return this;
130+
}
131+
111132
protected buildWhere(where: QueryWhere[]): {
112133
sql: string;
113134
binding: unknown[];
@@ -176,16 +197,26 @@ export class QueryBuilder {
176197
this.states.select.length === 0
177198
? '*'
178199
: this.states.select
179-
.map((field) => this.dialect.escapeIdentifier(field))
200+
.map((field) => this.escapeIdentifier(field))
180201
.join(',');
181202

182203
const { sql: whereSql, binding: whereBinding } = this.buildWhere(
183204
this.states.where
184205
);
185206

186207
binding = binding.concat(...whereBinding);
208+
209+
let limitPart: string | undefined = undefined;
210+
187211
if (this.states.limit) {
188-
binding.push(this.states.limit);
212+
if (this.states.offset) {
213+
binding.push(this.states.offset);
214+
binding.push(this.states.limit);
215+
limitPart = 'LIMIT ?,?';
216+
} else {
217+
binding.push(this.states.limit);
218+
limitPart = 'LIMIT ?';
219+
}
189220
}
190221

191222
const sql =
@@ -195,7 +226,7 @@ export class QueryBuilder {
195226
'FROM',
196227
this.dialect.escapeIdentifier(this.states.table),
197228
whereSql ? 'WHERE ' + whereSql : whereSql,
198-
this.states.limit ? `LIMIT ?` : null,
229+
limitPart,
199230
]
200231
.filter(Boolean)
201232
.join(' ') + ';';

src/renderer/components/Button/styles.module.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
span {
1919
display: block;
20+
white-space: nowrap;
2021
}
2122
}
2223

src/renderer/components/DatabaseTable/DatabaseTableList.tsx

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ import styles from './styles.module.scss';
22
import TreeView, { TreeViewItemData } from '../TreeView';
33
import { useState, useMemo, useCallback } from 'react';
44
import { useWindowTab } from 'renderer/contexts/WindowTabProvider';
5-
import QueryWindow from 'renderer/screens/DatabaseScreen/QueryWindow';
65
import { useSchema } from 'renderer/contexts/SchemaProvider';
7-
import { QueryBuilder } from 'libs/QueryBuilder';
86
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
97
import {
108
faCalendar,
11-
faCode,
129
faEye,
1310
faGear,
11+
faTableCells,
1412
faTableList,
1513
} from '@fortawesome/free-solid-svg-icons';
1614
import { useContextMenu } from 'renderer/contexts/ContextMenuProvider';
@@ -20,6 +18,7 @@ import DatabaseSelection from './DatabaseSelection';
2018
import ListViewEmptyState from '../ListView/ListViewEmptyState';
2119
import TextField from '../TextField';
2220
import { useDebounce } from 'hooks/useDebounce';
21+
import TableDataViewer from 'renderer/screens/DatabaseScreen/TableDataViewer';
2322

2423
type SelectedTreeViewItem = TreeViewItemData<{
2524
database: string;
@@ -39,27 +38,28 @@ export default function DatabaseTableList() {
3938
'triggers',
4039
]);
4140

42-
const select200RowCallback = useCallback((item: SelectedTreeViewItem) => {
43-
const tableName = item.data?.name;
44-
const type = item.data?.type;
45-
if ((type === 'table' || type === 'view') && tableName) {
46-
newWindow(
47-
`SELECT ${tableName}`,
48-
(key, name) => (
49-
<QueryWindow
50-
initialSql={new QueryBuilder('mysql')
51-
.table(tableName)
52-
.limit(200)
53-
.toRawSQL()}
54-
initialRun
55-
tabKey={key}
56-
name={name}
57-
/>
58-
),
59-
{ icon: <FontAwesomeIcon icon={faCode} /> }
60-
);
61-
}
62-
}, []);
41+
const viewTableData = useCallback(
42+
(item: SelectedTreeViewItem) => {
43+
const tableName = item.data?.name;
44+
const databaseName = item.data?.database;
45+
const type = item.data?.type;
46+
if ((type === 'table' || type === 'view') && tableName && databaseName) {
47+
newWindow(
48+
tableName,
49+
(key, name) => (
50+
<TableDataViewer
51+
tableName={tableName}
52+
databaseName={databaseName}
53+
tabKey={key}
54+
name={name}
55+
/>
56+
),
57+
{ icon: <FontAwesomeIcon icon={faTableCells} color="#9b59b6" /> }
58+
);
59+
}
60+
},
61+
[currentDatabase]
62+
);
6363

6464
const { handleContextMenu } = useContextMenu(() => {
6565
const tableName = selected?.data?.name;
@@ -74,8 +74,8 @@ export default function DatabaseTableList() {
7474
) {
7575
return [
7676
{
77-
text: 'Select 200 Rows',
78-
onClick: () => select200RowCallback(selected),
77+
text: 'View Data',
78+
onClick: () => viewTableData(selected),
7979
},
8080
{
8181
text: 'Open Structure',
@@ -212,7 +212,7 @@ export default function DatabaseTableList() {
212212
collapsedKeys={collapsed}
213213
onCollapsedChange={setCollapsed}
214214
onContextMenu={handleContextMenu}
215-
onDoubleClick={select200RowCallback}
215+
onDoubleClick={viewTableData}
216216
items={schemaListItem}
217217
/>
218218
) : (

src/renderer/components/Toolbar/index.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@ interface ToolbarItemProps {
1414
export default function Toolbar({
1515
children,
1616
shadow,
17-
}: PropsWithChildren<{ shadow?: boolean }>) {
17+
shadowTop,
18+
}: PropsWithChildren<{ shadow?: boolean; shadowTop?: boolean }>) {
19+
const className = [
20+
styles.toolbar,
21+
shadow ? styles.shadow : undefined,
22+
shadowTop ? styles.shadowTop : undefined,
23+
]
24+
.filter(Boolean)
25+
.join(' ');
26+
1827
return (
19-
<div
20-
className={shadow ? `${styles.toolbar} ${styles.shadow}` : styles.toolbar}
21-
>
28+
<div className={className}>
2229
<ul>{children}</ul>
2330
</div>
2431
);

src/renderer/components/Toolbar/styles.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
border-bottom: 2px solid var(--color-surface-hover);
33
}
44

5+
.shadowTop {
6+
box-shadow: var(--color-shadow) 0 1.95px 5px;
7+
}
8+
59
.filler {
610
flex-grow: 1;
711
}

src/renderer/contexts/WindowTabProvider.tsx

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ import { useDatabaseSetting } from './DatabaseSettingProvider';
1212
import { db } from 'renderer/db';
1313
import { DatabaseSavedState } from 'types/FileFormatType';
1414
import QueryWindow from 'renderer/screens/DatabaseScreen/QueryWindow';
15-
import SqlTableSchemaTab from 'renderer/screens/DatabaseScreen/SqlTableSchemaTab';
1615
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
17-
import { faCode, faTableList } from '@fortawesome/free-solid-svg-icons';
16+
import { faCode } from '@fortawesome/free-solid-svg-icons';
1817
import NotImplementCallback from 'libs/NotImplementCallback';
1918
import useBeforeClose from 'renderer/hooks/useBeforeClose';
2019

@@ -97,42 +96,43 @@ export function WindowTabProvider({ children }: PropsWithChildren) {
9796
db.table('database_tabs')
9897
.get(setting.id)
9998
.then((result: DatabaseSavedState | null) => {
99+
let emptyTab = true;
100+
100101
if (result) {
101-
setTabs(
102-
result.tabs.map((tab) => {
103-
let component: ReactElement = <div />;
104-
let icon: ReactElement = <FontAwesomeIcon icon={faCode} />;
105-
106-
if (tab.type === 'query' || !tab.type) {
107-
component = (
108-
<QueryWindow
109-
tabKey={tab.key}
110-
name={tab.name}
111-
initialSql={tab.sql}
112-
/>
113-
);
114-
} else if (tab.type === 'table-schema') {
115-
component = (
116-
<SqlTableSchemaTab
117-
tabKey={tab.key}
118-
name={tab.name}
119-
database={tab.database ?? ''}
120-
table={tab.table ?? ''}
121-
/>
122-
);
123-
icon = <FontAwesomeIcon icon={faTableList} color="#3498db" />;
124-
}
125-
126-
return {
127-
key: tab.key,
128-
name: tab.name,
129-
icon,
130-
component,
131-
};
132-
})
133-
);
134-
setSelectedTab(result.selectedTabKey);
135-
} else {
102+
const tabs = result.tabs.map((tab) => {
103+
let component: ReactElement = <div />;
104+
const icon: ReactElement = <FontAwesomeIcon icon={faCode} />;
105+
106+
if (tab.type === 'query' || !tab.type) {
107+
component = (
108+
<QueryWindow
109+
tabKey={tab.key}
110+
name={tab.name}
111+
initialSql={tab.sql}
112+
/>
113+
);
114+
}
115+
116+
return {
117+
key: tab.key,
118+
name: tab.name,
119+
icon,
120+
component,
121+
};
122+
});
123+
124+
if (tabs.length > 0) {
125+
emptyTab = true;
126+
setTabs(tabs);
127+
setSelectedTab(
128+
tabs.find((tab) => tab.key === result.selectedTabKey)
129+
? result.selectedTabKey
130+
: tabs[0].key
131+
);
132+
}
133+
}
134+
135+
if (emptyTab) {
136136
const key = uuidv1();
137137
setTabs([
138138
{

src/renderer/screens/DatabaseScreen/QueryHeader.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import Button from 'renderer/components/Button';
22
import QueryWindowNameEditor from './QueryWindowNameEditor';
33
import styles from './QueryHeader.module.scss';
4+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5+
import { faSave } from '@fortawesome/free-solid-svg-icons';
46

57
export default function QueryHeader({
68
tabKey,
@@ -12,7 +14,7 @@ export default function QueryHeader({
1214
return (
1315
<div className={styles.queryHeader}>
1416
<QueryWindowNameEditor tabKey={tabKey} />
15-
<Button primary onClick={onSave}>
17+
<Button primary onClick={onSave} icon={<FontAwesomeIcon icon={faSave} />}>
1618
Save
1719
</Button>
1820
</div>

0 commit comments

Comments
 (0)