Skip to content

Commit 5de8a0c

Browse files
committed
feat: add database picker
1 parent 5a7bd24 commit 5de8a0c

File tree

16 files changed

+250
-48
lines changed

16 files changed

+250
-48
lines changed

src/drivers/common/MySQLCommonInterface.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ export default class MySQLCommonInterface extends SQLCommonInterface {
1717
this.currentDatabaseName = currentDatabaseName;
1818
}
1919

20+
async switchDatabase(databaseName: string): Promise<boolean> {
21+
const response = await this.runner.execute(
22+
[{ sql: 'USE ' + qb().escapeId(databaseName) }],
23+
{
24+
skipProtection: true,
25+
}
26+
);
27+
28+
if (response[0].result.error) {
29+
return false;
30+
}
31+
32+
return true;
33+
}
34+
2035
async getSchema(): Promise<DatabaseSchemas> {
2136
const response = await this.runner.execute(
2237
[

src/drivers/common/NotImplementCommonInterface.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ export default class NotImplementCommonInterface extends SQLCommonInterface {
99
async getTableSchema(): Promise<TableDefinitionSchema> {
1010
throw 'Not implemented';
1111
}
12+
13+
async switchDatabase(): Promise<boolean> {
14+
throw 'Not implemented';
15+
}
1216
}

src/drivers/common/SQLCommonInterface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import { DatabaseSchemas, TableDefinitionSchema } from 'types/SqlSchema';
33
export default abstract class SQLCommonInterface {
44
abstract getSchema(): Promise<DatabaseSchemas>;
55
abstract getTableSchema(table: string): Promise<TableDefinitionSchema>;
6+
abstract switchDatabase(database: string): Promise<boolean>;
67
}

src/libs/QueryBuilder.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export class QueryBuilder {
5555
}
5656
}
5757

58+
escapeId(name: string) {
59+
return this.dialect.escapeIdentifier(name);
60+
}
61+
5862
table(name: string) {
5963
this.states.table = name;
6064
return this;

src/renderer/App.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ body.dark {
5858
--color-surface: #1f1f1f;
5959
--color-surface-hover: #3f3f3f;
6060

61-
--color-shadow: rgba(255, 255, 255, 0.55);
61+
--color-shadow: rgba(200, 200, 200, 0.2);
6262
--color-border: #aaa;
6363

6464
--color-text: #fff;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { useState, useMemo, useCallback } from 'react';
2+
import { faChevronRight, faDatabase } from '@fortawesome/free-solid-svg-icons';
3+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4+
import styles from './styles.module.scss';
5+
import { useSchmea } from 'renderer/contexts/SchemaProvider';
6+
import Modal from '../Modal';
7+
import ListView from '../ListView';
8+
import Button from '../Button';
9+
import { useSqlExecute } from 'renderer/contexts/SqlExecuteProvider';
10+
11+
interface DatabaseSelectionModalProps {
12+
open: boolean;
13+
onClose: () => void;
14+
}
15+
16+
function DatabaseSelectionModal({
17+
onClose,
18+
open,
19+
}: DatabaseSelectionModalProps) {
20+
const { currentDatabase, schema } = useSchmea();
21+
22+
const databaseList = useMemo(() => {
23+
if (schema) {
24+
return Object.keys(schema);
25+
}
26+
return [];
27+
}, [schema]);
28+
29+
const [selectedDatabase, setSelectedDatabase] = useState<string | undefined>(
30+
currentDatabase || undefined
31+
);
32+
33+
const { common } = useSqlExecute();
34+
35+
const onOpenClicked = useCallback(() => {
36+
if (selectedDatabase !== currentDatabase && selectedDatabase) {
37+
common
38+
.switchDatabase(selectedDatabase)
39+
.then((result) => {
40+
if (result) {
41+
onClose();
42+
}
43+
})
44+
.catch(console.error);
45+
}
46+
}, [common, selectedDatabase, currentDatabase, onClose]);
47+
48+
return (
49+
<Modal open={open} title="Database Selection" onClose={onClose}>
50+
<Modal.Body>
51+
<div
52+
style={{ maxHeight: '50vh', overflowY: 'auto' }}
53+
className={'scroll'}
54+
>
55+
<ListView
56+
selectedItem={selectedDatabase}
57+
onSelectChange={(item) => setSelectedDatabase(item)}
58+
items={databaseList}
59+
extractMeta={(database) => ({
60+
text: database,
61+
key: database,
62+
icon: <FontAwesomeIcon icon={faDatabase} />,
63+
})}
64+
/>
65+
</div>
66+
</Modal.Body>
67+
<Modal.Footer>
68+
<Button primary onClick={onOpenClicked} disabled={!selectedDatabase}>
69+
Open
70+
</Button>
71+
</Modal.Footer>
72+
</Modal>
73+
);
74+
}
75+
76+
export default function DatabaseSelection() {
77+
const { currentDatabase } = useSchmea();
78+
const [open, setOpen] = useState(false);
79+
80+
const onClose = useCallback(() => {
81+
setOpen(false);
82+
}, [setOpen]);
83+
84+
const onOpen = useCallback(() => {
85+
setOpen(true);
86+
}, [setOpen]);
87+
88+
return (
89+
<>
90+
<div className={styles.header} onClick={onOpen}>
91+
<FontAwesomeIcon icon={faDatabase} color="#27ae60" />
92+
<span>{currentDatabase}</span>
93+
<FontAwesomeIcon icon={faChevronRight} />
94+
</div>
95+
<DatabaseSelectionModal onClose={onClose} open={open} />
96+
</>
97+
);
98+
}

src/renderer/components/DatabaseTable/DatabaseTable.module.scss

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/renderer/components/DatabaseTable/DatabaseTableList.tsx

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import styles from './DatabaseTable.module.scss';
1+
import styles from './styles.module.scss';
22
import TreeView, { TreeViewItemData } from '../TreeView';
33
import { useState, useMemo } from 'react';
44
import { useWindowTab } from 'renderer/contexts/WindowTabProvider';
@@ -14,6 +14,8 @@ import {
1414
} from '@fortawesome/free-solid-svg-icons';
1515
import { useContextMenu } from 'renderer/contexts/ContextMenuProvider';
1616
import SqlTableSchemaTab from 'renderer/screens/DatabaseScreen/SqlTableSchemaTab';
17+
import Layout from '../Layout';
18+
import DatabaseSelection from './DatabaseSelection';
1719

1820
export default function DatabaseTableList() {
1921
const { schema, currentDatabase } = useSchmea();
@@ -133,31 +135,39 @@ export default function DatabaseTableList() {
133135

134136
return (
135137
<div className={styles.tables}>
136-
<TreeView
137-
selected={selected}
138-
onSelectChange={setSelected}
139-
collapsedKeys={collapsed}
140-
onCollapsedChange={setCollapsed}
141-
onContextMenu={handleContextMenu}
142-
onDoubleClick={(item) => {
143-
const tableName = item.data?.name;
144-
const type = item.data?.type;
145-
if ((type === 'table' || type === 'view') && tableName) {
146-
newWindow(`SELECT ${tableName}`, (key, name) => (
147-
<QueryWindow
148-
initialSql={new QueryBuilder('mysql')
149-
.table(tableName)
150-
.limit(200)
151-
.toRawSQL()}
152-
initialRun
153-
tabKey={key}
154-
name={name}
155-
/>
156-
));
157-
}
158-
}}
159-
items={schemaListItem}
160-
/>
138+
<Layout>
139+
<Layout.Fixed shadowBottom>
140+
<DatabaseSelection />
141+
</Layout.Fixed>
142+
<Layout.Grow>
143+
<TreeView
144+
selected={selected}
145+
onSelectChange={setSelected}
146+
collapsedKeys={collapsed}
147+
onCollapsedChange={setCollapsed}
148+
onContextMenu={handleContextMenu}
149+
onDoubleClick={(item) => {
150+
const tableName = item.data?.name;
151+
const type = item.data?.type;
152+
if ((type === 'table' || type === 'view') && tableName) {
153+
newWindow(`SELECT ${tableName}`, (key, name) => (
154+
<QueryWindow
155+
initialSql={new QueryBuilder('mysql')
156+
.table(tableName)
157+
.limit(200)
158+
.toRawSQL()}
159+
initialRun
160+
tabKey={key}
161+
name={name}
162+
/>
163+
));
164+
}
165+
}}
166+
items={schemaListItem}
167+
/>
168+
</Layout.Grow>
169+
<Layout.Fixed>Footer</Layout.Fixed>
170+
</Layout>
161171
</div>
162172
);
163173
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.tables {
2+
background: var(--color-list-surface);
3+
height: 100%
4+
}
5+
6+
.header {
7+
padding: 8px 15px;
8+
display: flex;
9+
flex-direction: row;
10+
gap: 10px;
11+
align-items: center;
12+
cursor: pointer;
13+
14+
span {
15+
flex-grow: 1;
16+
}
17+
}

src/renderer/components/Layout/index.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { PropsWithChildren } from 'react';
1+
import { PropsWithChildren, useMemo } from 'react';
2+
import styles from './styles.module.scss';
3+
4+
interface LayoutFixedProps {
5+
shadowTop?: boolean;
6+
shadowBottom?: boolean;
7+
}
28

39
export default function Layout({ children }: PropsWithChildren) {
410
return (
@@ -16,9 +22,26 @@ export default function Layout({ children }: PropsWithChildren) {
1622
}
1723

1824
Layout.Grow = function ({ children }: PropsWithChildren) {
19-
return <div style={{ flexGrow: 1, position: 'relative' }}>{children}</div>;
25+
return (
26+
<div style={{ flexGrow: 1, position: 'relative', overflow: 'hidden' }}>
27+
{children}
28+
</div>
29+
);
2030
};
2131

22-
Layout.Fixed = function ({ children }: PropsWithChildren) {
23-
return <div style={{ flexShrink: 0, flexGrow: 0 }}>{children}</div>;
32+
Layout.Fixed = function ({
33+
children,
34+
shadowBottom,
35+
}: PropsWithChildren<LayoutFixedProps>) {
36+
const className = useMemo(() => {
37+
return [shadowBottom ? styles.shadowBottom : undefined]
38+
.filter(Boolean)
39+
.join();
40+
}, [shadowBottom]);
41+
42+
return (
43+
<div className={className} style={{ flexShrink: 0, flexGrow: 0 }}>
44+
{children}
45+
</div>
46+
);
2447
};

0 commit comments

Comments
 (0)