Skip to content

Commit e00f790

Browse files
committed
feat: add search result and time to execute query
1 parent fb20462 commit e00f790

File tree

10 files changed

+258
-99
lines changed

10 files changed

+258
-99
lines changed

src/hooks/useDebounce.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,18 @@ export function useDebounce<T>(value: T, delay: number) {
2020
);
2121
return debouncedValue;
2222
}
23+
24+
export function useDebounceEffect(
25+
cb: () => void,
26+
deps: unknown[],
27+
delay: number
28+
) {
29+
useEffect(() => {
30+
const handler = setTimeout(() => {
31+
cb();
32+
}, delay);
33+
return () => {
34+
clearTimeout(handler);
35+
};
36+
}, [...deps, delay]);
37+
}

src/libs/SqlRunnerManager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type BeforeEachEventCallback = (
1919

2020
export interface SqlStatementResult {
2121
statement: SqlStatement;
22+
time: number;
2223
result: QueryResult;
2324
}
2425

@@ -85,6 +86,7 @@ export class SqlRunnerManager {
8586

8687
console.log(statement.sql);
8788

89+
const startTime = Date.now();
8890
const returnedResult = await this.executor(
8991
statement.sql,
9092
statement.params
@@ -93,6 +95,7 @@ export class SqlRunnerManager {
9395
if (!returnedResult?.error) {
9496
result.push({
9597
statement,
98+
time: Date.now() - startTime,
9699
result: returnedResult,
97100
});
98101
} else {

src/renderer/components/Toolbar/index.tsx

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { useContextMenu } from 'renderer/contexts/ContextMenuProvider';
55

66
interface ToolbarItemProps {
77
icon?: ReactNode;
8-
text: string;
8+
text?: string;
9+
badge?: number;
10+
disabled?: boolean;
911
onClick?: () => void;
1012
}
1113

@@ -22,11 +24,62 @@ export default function Toolbar({
2224
);
2325
}
2426

25-
Toolbar.Item = function ({ icon, text, onClick }: ToolbarItemProps) {
27+
Toolbar.Item = function ({
28+
icon,
29+
text,
30+
onClick,
31+
badge,
32+
disabled,
33+
}: ToolbarItemProps) {
2634
return (
27-
<li onClick={onClick}>
35+
<li
36+
className={styles.button}
37+
onClick={disabled ? undefined : onClick}
38+
style={{ opacity: disabled ? 0.5 : 1 }}
39+
>
2840
{icon && <span className={styles.icon}>{icon}</span>}
29-
<span>{text}</span>
41+
{text && <span>{text}</span>}
42+
{badge ? <span className={styles.badge}>{badge}</span> : <></>}
43+
</li>
44+
);
45+
};
46+
47+
Toolbar.Filler = function () {
48+
return <li className={styles.filler}></li>;
49+
};
50+
51+
Toolbar.Text = function ({ children }: PropsWithChildren) {
52+
return <li>{children}</li>;
53+
};
54+
55+
Toolbar.Separator = function () {
56+
return <li className={styles.separator} />;
57+
};
58+
59+
interface ToolbarTextFieldProps {
60+
value?: string;
61+
placeholder?: string;
62+
onChange?: (v: string) => void;
63+
}
64+
65+
Toolbar.TextField = function ({
66+
placeholder,
67+
value,
68+
onChange,
69+
}: ToolbarTextFieldProps) {
70+
return (
71+
<li className={styles.textfield}>
72+
<input
73+
spellCheck={false}
74+
autoCorrect="off"
75+
autoComplete="off"
76+
type="text"
77+
placeholder={placeholder}
78+
value={value}
79+
onChange={(e) => {
80+
if (onChange) onChange(e.currentTarget.value);
81+
}}
82+
/>
3083
</li>
3184
);
3285
};
@@ -43,7 +96,7 @@ Toolbar.ContextMenu = function ({
4396
const { handleClick } = useContextMenu(() => items, [items]);
4497

4598
return (
46-
<li onClick={handleClick}>
99+
<li className={styles.button} onClick={handleClick}>
47100
{icon && <span className={styles.icon}>{icon}</span>}
48101
<span>{text}</span>
49102
</li>

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

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

5+
.filler {
6+
flex-grow: 1;
7+
}
8+
9+
.button {
10+
cursor: pointer;
11+
border-radius: 5px;
12+
color: var(--color-text);
13+
14+
span.icon {
15+
display: inline-block;
16+
color: #27ae60;
17+
margin-right: 8px;
18+
}
19+
20+
span.icon:last-child {
21+
margin-right: 0;
22+
}
23+
24+
span.badge {
25+
margin-left: 8px;
26+
background: var(--color-critical);
27+
color: #fff;
28+
display: inline-block;
29+
padding: 0px 5px;
30+
border-radius: 4px;
31+
font-size: 0.8rem;
32+
}
33+
34+
&:hover {
35+
background: var(--color-surface-light);
36+
}
37+
}
38+
39+
.separator {
40+
padding: 0;
41+
margin: 0 3px;
42+
border-right: 1px solid var(--color-border)
43+
}
44+
45+
.textfield {
46+
padding: 0 !important;
47+
48+
input {
49+
padding: 5px 10px;
50+
border-radius: 4px;
51+
margin: 0 5px;
52+
border: 1px solid var(--color-border);
53+
background: inherit;
54+
outline: none;
55+
color: inherit;
56+
}
57+
}
58+
559
.toolbar {
660
ul {
761
list-style-type: none;
@@ -11,21 +65,8 @@
1165
li {
1266
display: flex;
1367
padding: 5px 10px;
14-
cursor: pointer;
15-
border-radius: 5px;
16-
color: var(--color-text);
1768
justify-content: center;
1869
align-items: center;
19-
20-
&:hover {
21-
background: var(--color-surface-light);
22-
}
23-
24-
span.icon {
25-
display: inline-block;
26-
color: #27ae60;
27-
margin-right: 8px;
28-
}
2970
}
3071
}
3172
}

src/renderer/screens/DatabaseScreen/QueryMultipleResultViewer.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,15 @@ export default memo(function QueryMultipleResultViewer({
1717

1818
if (queryResultOnly.length === 0) return <div />;
1919
if (queryResultOnly.length === 1)
20-
return (
21-
<QueryResultViewer
22-
result={value[0].result}
23-
statement={value[0].statement}
24-
/>
25-
);
20+
return <QueryResultViewer statementResult={value[0]} />;
2621

2722
return (
2823
<WindowTab
2924
selected={selected}
3025
onTabChanged={(tab) => setSelected(tab.key)}
3126
tabs={queryResultOnly.map((result, idx) => {
3227
return {
33-
component: (
34-
<QueryResultViewer
35-
result={result.result}
36-
statement={result.statement}
37-
/>
38-
),
28+
component: <QueryResultViewer statementResult={result} />,
3929
key: `query_${idx}`,
4030
name: `Query ${idx + 1}`,
4131
};

src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultAction.tsx

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,62 @@ import { useCallback, useState, useEffect } from 'react';
22
import applyQueryResultChanges from 'libs/ApplyQueryResultChanges';
33
import generateSqlFromChanges from 'libs/GenerateSqlFromChanges';
44
import generateSqlFromPlan from 'libs/GenerateSqlFromPlan';
5-
import Button from 'renderer/components/Button';
65
import { useQueryResultChange } from 'renderer/contexts/QueryResultChangeProvider';
76
import { useSchema } from 'renderer/contexts/SchemaProvider';
87
import { useSqlExecute } from 'renderer/contexts/SqlExecuteProvider';
98
import { QueryResult } from 'types/SqlResult';
109
import styles from './styles.module.scss';
11-
import ButtonGroup from 'renderer/components/ButtonGroup';
1210
import ExportModal from '../ExportModal';
1311
import { useDialog } from 'renderer/contexts/DialogProvider';
12+
import Toolbar from 'renderer/components/Toolbar';
13+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
14+
import {
15+
faChevronLeft,
16+
faChevronRight,
17+
} from '@fortawesome/free-solid-svg-icons';
18+
import { useDebounceEffect } from 'hooks/useDebounce';
1419

1520
interface QueryResultActionProps {
1621
result: QueryResult;
22+
resultAfterFilter: { data: Record<string, unknown>; rowIndex: number }[];
1723
onResultChange: React.Dispatch<React.SetStateAction<QueryResult>>;
24+
onSearchChange: (v: string) => void;
1825
onRequestRefetch: () => void;
1926
page: number;
2027
pageSize: number;
2128
onPageChange: React.Dispatch<React.SetStateAction<number>>;
29+
time: number;
2230
}
2331

2432
export default function QueryResultAction({
2533
result,
34+
resultAfterFilter,
2635
onResultChange,
2736
onRequestRefetch,
2837
page,
2938
pageSize,
3039
onPageChange,
40+
onSearchChange,
41+
time,
3142
}: QueryResultActionProps) {
3243
const { showErrorDialog } = useDialog();
3344
const [changeCount, setChangeCount] = useState(0);
3445
const [showExportModal, setShowExportModal] = useState(false);
3546
const { clearChange, collector } = useQueryResultChange();
3647
const { schema, currentDatabase } = useSchema();
48+
const [search, setSearch] = useState('');
3749
const { runner } = useSqlExecute();
3850

3951
const rowStart = page * pageSize;
40-
const rowEnd = Math.min(result.rows.length, rowStart + pageSize);
52+
const rowEnd = Math.min(resultAfterFilter.length, rowStart + pageSize);
53+
54+
useDebounceEffect(
55+
() => {
56+
onSearchChange(search);
57+
},
58+
[onSearchChange, search],
59+
1000
60+
);
4161

4262
useEffect(() => {
4363
const cb = (count: number) => {
@@ -59,7 +79,6 @@ export default function QueryResultAction({
5979
collector.getChanges()
6080
);
6181

62-
console.log(plans);
6382
const rawSql = plans.map((plan) => ({
6483
sql: generateSqlFromPlan(plan),
6584
}));
@@ -96,40 +115,46 @@ export default function QueryResultAction({
96115

97116
return (
98117
<div className={styles.footer}>
99-
{showExportModal && (
100-
<ExportModal data={result} onClose={() => setShowExportModal(false)} />
101-
)}
118+
<Toolbar>
119+
<Toolbar.Text>Took {Math.round(time / 1000)}s</Toolbar.Text>
120+
<Toolbar.Separator />
121+
<Toolbar.Item
122+
text="Commit"
123+
badge={changeCount}
124+
onClick={onCommit}
125+
disabled={!changeCount}
126+
/>
127+
<Toolbar.Item text="Export" onClick={() => setShowExportModal(true)} />
102128

103-
<div className={styles.footerAction}>
104-
<ButtonGroup>
105-
<Button primary={!!changeCount} onClick={onCommit}>
106-
{changeCount ? `Commit (${changeCount})` : 'Commit'}
107-
</Button>
108-
<Button primary onClick={() => setShowExportModal(true)}>
109-
Export
110-
</Button>
111-
</ButtonGroup>
112-
</div>
129+
<Toolbar.Separator />
130+
<Toolbar.TextField
131+
placeholder="Search here"
132+
value={search}
133+
onChange={setSearch}
134+
/>
135+
<Toolbar.Filler />
113136

114-
<div className={styles.footerPage}>
115-
<Button
137+
{/* Pagination */}
138+
<Toolbar.Item
139+
text=""
140+
icon={<FontAwesomeIcon icon={faChevronLeft} />}
116141
disabled={page === 0}
117-
primary
118142
onClick={() => onPageChange(page - 1)}
119-
>
120-
Prev
121-
</Button>
122-
<div>
123-
&nbsp;&nbsp;{rowStart}-{rowEnd} / {result.rows.length}&nbsp;&nbsp;
124-
</div>
125-
<Button
126-
primary
127-
disabled={rowStart + pageSize >= result.rows.length}
143+
/>
144+
<Toolbar.Text>
145+
{rowStart}-{rowEnd} / {resultAfterFilter.length}
146+
</Toolbar.Text>
147+
<Toolbar.Item
128148
onClick={() => onPageChange(page + 1)}
129-
>
130-
Next
131-
</Button>
132-
</div>
149+
text=""
150+
icon={<FontAwesomeIcon icon={faChevronRight} />}
151+
disabled={rowStart + pageSize >= resultAfterFilter.length}
152+
/>
153+
</Toolbar>
154+
155+
{showExportModal && (
156+
<ExportModal data={result} onClose={() => setShowExportModal(false)} />
157+
)}
133158
</div>
134159
);
135160
}

src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultLoading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function QueryResultLoading() {
2525
return (
2626
<div className={styles.result}>
2727
<div className={styles.container}></div>
28-
<div className={styles.footer}>
28+
<div className={styles.footer} style={{ padding: '5px 10px' }}>
2929
<Stack>
3030
<div
3131
style={{

0 commit comments

Comments
 (0)