Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 0f31aee

Browse files
committed
feat: use select and update
1 parent 5add3e9 commit 0f31aee

File tree

4 files changed

+297
-34
lines changed

4 files changed

+297
-34
lines changed

ui/src/components/DBDataGrid.tsx

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,97 @@
11
import { DataGridPro, GridColumns, GridRowsProp } from "@mui/x-data-grid-pro";
2+
import { useSelectTable } from "../hooks/useSelectTable";
3+
import { useUpdate } from "../hooks/useUpdate";
4+
import { IDBConnection } from "../utils/types";
25

3-
export function DBDataGrid() {
6+
export function DBDataGrid({
7+
database,
8+
table,
9+
}: {
10+
database: IDBConnection;
11+
table: string;
12+
}) {
13+
const { rows, columns, getTableRows } = useSelectTable(database, table);
14+
const { updateField } = useUpdate(database, table);
15+
const rowsWithInternalId = rows.map((row: any, index) => ({
16+
...row,
17+
internalId: index,
18+
}));
419
return (
520
<DataGridPro
6-
rows={rows}
7-
columns={columns}
8-
experimentalFeatures={{ newEditingApi: true }}
21+
rows={rowsWithInternalId}
22+
columns={parseColumns(columns)}
923
disableColumnFilter
1024
disableColumnMenu
1125
disableColumnSelector
1226
disableDensitySelector
27+
disableSelectionOnClick
28+
getRowId={(row) => row.internalId}
1329
sx={{
14-
px: 2,
30+
pr: 3,
31+
"& .light-bg": {
32+
backgroundColor: (theme) => theme.palette.background.paper,
33+
},
34+
}}
35+
onCellEditCommit={(props) => {
36+
const modifiedRow: any = rowsWithInternalId.find(
37+
(row: any) => row.internalId === props.id
38+
);
39+
const newValue = props.value;
40+
const field = props.field;
41+
const { internalId, ...row } = modifiedRow;
42+
updateField(row, field, newValue).then(() => {
43+
getTableRows();
44+
});
1545
}}
1646
/>
1747
);
1848
}
1949

20-
const columns: GridColumns = [
21-
{ field: "id", headerName: "Id", editable: true, sortable: false, flex: 1 },
22-
{
23-
field: "name",
24-
headerName: "Name",
25-
editable: true,
26-
sortable: false,
27-
flex: 1,
28-
},
29-
{
30-
field: "description",
31-
headerName: "Description",
50+
const parseColumns = (columnNames: string[]): GridColumns => {
51+
const zeroColumn: GridColumns = [
52+
{
53+
field: "internalId",
54+
headerName: "",
55+
hide: false,
56+
sortable: false,
57+
editable: false,
58+
flex: 0,
59+
width: 0,
60+
headerClassName: "light-bg",
61+
cellClassName: "light-bg",
62+
},
63+
];
64+
const columns = columnNames.map((columnName) => ({
65+
field: columnName,
66+
headerName: columnName.toUpperCase(),
67+
headerClassName: "light-bg",
3268
editable: true,
3369
sortable: false,
3470
flex: 1,
35-
},
36-
];
71+
}));
72+
return zeroColumn.concat(columns);
73+
};
3774

38-
const rows: GridRowsProp = [
39-
{ id: "1", name: "test name", description: "test description" },
40-
];
75+
const getColumnsFromRows = (rows: any[]) => {
76+
const columns: GridColumns = [
77+
{
78+
field: "internalId",
79+
headerName: "",
80+
hide: false,
81+
flex: 0,
82+
},
83+
];
84+
if (rows.length > 0) {
85+
const row = rows[0];
86+
for (const key in row) {
87+
columns.push({
88+
field: key,
89+
headerName: key.toUpperCase(),
90+
editable: true,
91+
sortable: false,
92+
flex: 1,
93+
});
94+
}
95+
}
96+
return columns;
97+
};

ui/src/components/DatabaseViewerDialog.tsx

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,16 @@ export default function DatabaseViewerDialog(props: Props) {
8989

9090
return (
9191
<div>
92-
<Dialog {...dialogProps}>
92+
<Dialog {...dialogProps} sx={{
93+
"& .MuiDialog-paper": {
94+
border: 0,
95+
backgroundColor: (theme) => theme.palette.background.default,
96+
},
97+
"& .MuiToolbar-root": {
98+
backgroundColor: (theme) => theme.palette.background.default,
99+
border: 0,
100+
}
101+
}}>
93102
<AppBar sx={{ position: "relative" }}>
94103
<Toolbar>
95104
<IconButton
@@ -109,7 +118,9 @@ export default function DatabaseViewerDialog(props: Props) {
109118
<IconButton
110119
edge="start"
111120
color="inherit"
112-
onClick={() => getDBTables()}
121+
onClick={() => {
122+
getDBTables();
123+
}}
113124
aria-label="sync"
114125
>
115126
<Sync
@@ -141,26 +152,27 @@ export default function DatabaseViewerDialog(props: Props) {
141152
height: "100%",
142153
}}
143154
>
144-
<Grid container spacing={3} height="100%">
145-
<Grid item xs>
155+
<Grid container gap={3} height="100%">
156+
<Grid item xs sx={{
157+
paddingY: 1,
158+
background: (theme) => theme.palette.background.paper,
159+
}}>
146160
<Box>
147161
<TreeView
148162
aria-label="file system navigator"
149163
defaultCollapseIcon={<Storage />}
150164
defaultExpandIcon={<Storage />}
165+
defaultEndIcon={<Storage />}
151166
multiSelect={false}
152167
onNodeSelect={handleOpenTable}
153168
>
154-
{tables.map((table, key) => (
155-
<React.Fragment key={key}>
156-
<TreeItem nodeId={table} label={table} />
157-
{key < tables.length - 1 && <Divider />}
158-
</React.Fragment>
169+
{tables.map((table) => (
170+
<TreeItem key={table} nodeId={table} label={table} sx={{paddingY: 0.5}}/>
159171
))}
160172
</TreeView>
161173
</Box>
162174
</Grid>
163-
<Grid item xs={9}>
175+
<Grid xs={9}>
164176
<>
165177
{tabs.length === 0 && <NoRowsOverlay />}
166178
{tabs.length > 0 && (
@@ -182,13 +194,23 @@ export default function DatabaseViewerDialog(props: Props) {
182194
iconPosition="end"
183195
label={tab.name}
184196
value={tab.table}
197+
sx={{
198+
minHeight: 0,
199+
}}
185200
/>
186201
))}
187202
</TabList>
188203
</Box>
189204
{tabs.map((tab, key) => (
190-
<TabPanel value={tab.name} key={key}>
191-
<DBDataGrid />
205+
<TabPanel
206+
value={tab.name}
207+
key={key}
208+
sx={{
209+
height: "92%",
210+
padding: 0,
211+
}}
212+
>
213+
<DBDataGrid database={database} table={tab.name} />
192214
</TabPanel>
193215
))}
194216
</TabContext>

ui/src/hooks/useSelectTable.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { createDockerDesktopClient } from "@docker/extension-api-client";
2+
import { useEffect, useState } from "react";
3+
import { IDBConnection } from "../utils/types";
4+
5+
const ddClient = createDockerDesktopClient();
6+
7+
interface Column {
8+
Name: string;
9+
Type: string;
10+
Null: string;
11+
Default: string;
12+
}
13+
14+
export const useSelectTable = (database: IDBConnection, table: string) => {
15+
const [columns, setColumns] = useState<string[]>([]);
16+
const [rows, setRows] = useState<unknown[]>([]);
17+
const [loading, setLoading] = useState<boolean>(false);
18+
const [error, setError] = useState<string | null>(null);
19+
20+
const getTableColumns = async () => {
21+
setLoading(true);
22+
try {
23+
const result = await ddClient.extension.host!.cli.exec("usql", [
24+
database.connectionString,
25+
"-J", // json output
26+
"-q", // quiet, do not print connection string
27+
"-c", // execute the command
28+
"'\\d " + table + "'",
29+
]);
30+
31+
const columns = result.parseJsonObject() as Column[];
32+
console.log(columns);
33+
setColumns(columns.map((column) => column.Name));
34+
} catch (err) {
35+
console.log(err);
36+
//setError(err?.message);
37+
}
38+
setLoading(false);
39+
};
40+
41+
const getTableRows = async () => {
42+
setLoading(true);
43+
try {
44+
const result = await ddClient.extension.host!.cli.exec("usql", [
45+
database.connectionString,
46+
"-J", // json output
47+
"-q", // quiet, do not print connection string
48+
"-c", // execute the command
49+
getSelectClause(database.image, table),
50+
// "'\\d " + table + "'",
51+
]);
52+
console.log(database, table);
53+
const rows = result.parseJsonObject() as unknown[];
54+
// const rows = result.parseJsonObject() as Column[];
55+
// setrows(rows.map((column) => column.Name));
56+
setRows(parseRowsData(rows, database.image));
57+
} catch (err) {
58+
console.log(err);
59+
//setError(err?.message);
60+
}
61+
setLoading(false);
62+
};
63+
64+
useEffect(() => {
65+
getTableColumns();
66+
getTableRows();
67+
}, [table]);
68+
69+
return { getTableRows, columns, rows, loading, error };
70+
};
71+
72+
const getSelectClause = (database: string, table: string) => {
73+
switch (database) {
74+
case "postgres":
75+
return getPostgresSelectClause(table);
76+
case "mysql":
77+
return getPostgresSelectClause(table);
78+
default:
79+
return getPostgresSelectClause(table);
80+
}
81+
};
82+
83+
const getPostgresSelectClause = (table: string) => {
84+
return "'select * from " + table + "'";
85+
};
86+
87+
const parseRowsData = (rows: unknown[], database: string) => {
88+
switch (database) {
89+
case "postgres":
90+
return parsePostgresRowsData(rows);
91+
case "mysql":
92+
return parseMysqlRowsData(rows);
93+
default:
94+
return rows;
95+
}
96+
};
97+
98+
const parsePostgresRowsData = (rows: unknown[]) => {
99+
return rows;
100+
};
101+
102+
const parseMysqlRowsData = (rows: unknown[]) => {
103+
return rows;
104+
};

ui/src/hooks/useUpdate.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { createDockerDesktopClient } from "@docker/extension-api-client";
2+
import { IDBConnection } from "../utils/types";
3+
4+
const ddClient = createDockerDesktopClient();
5+
6+
export const useUpdate = (database: IDBConnection, table: string) => {
7+
const updateField = async (
8+
modifiedRow: Record<string, any>,
9+
field: string,
10+
value: string
11+
) => {
12+
try {
13+
const result = await ddClient.extension.host!.cli.exec("usql", [
14+
database.connectionString,
15+
"-J", // json output
16+
"-q", // quiet, do not print connection string
17+
"-c", // execute the command
18+
getUpdateClause(modifiedRow, database.image, table, field, value),
19+
]);
20+
console.log(result);
21+
return result.stdout;
22+
} catch (err: any) {
23+
console.log(err);
24+
ddClient.desktopUI.toast.error(err.stderr);
25+
//setError(err?.message);
26+
}
27+
};
28+
return { updateField };
29+
};
30+
31+
const getUpdateClause = (
32+
modifiedRow: Record<string, any>,
33+
database: string,
34+
table: string,
35+
field: string,
36+
value: any
37+
) => {
38+
switch (database) {
39+
case "postgres":
40+
return getPostgresUpdateClause(modifiedRow, table, field, value);
41+
case "mysql":
42+
return getPostgresUpdateClause(modifiedRow, table, field, value);
43+
default:
44+
return getPostgresUpdateClause(modifiedRow, table, field, value);
45+
}
46+
};
47+
48+
const getPostgresUpdateClause = (
49+
modifiedRow: Record<string, any>,
50+
table: string,
51+
field: string,
52+
value: any
53+
) => {
54+
return (
55+
'"update ' +
56+
table +
57+
" set " +
58+
field +
59+
" = " +
60+
getValueOrString(value) +
61+
" where " +
62+
getWhereClause(modifiedRow) +
63+
'"'
64+
);
65+
};
66+
67+
const getValueOrString = (value: string) => {
68+
if (typeof value === "string") {
69+
return "'" + value + "'";
70+
}
71+
return value;
72+
};
73+
74+
const getWhereClause = (modifiedRow: Record<string, any>) => {
75+
let whereClause = [];
76+
for (const [key, value] of Object.entries(modifiedRow)) {
77+
whereClause.push(key + " = " + getValueOrString(value));
78+
}
79+
return whereClause.join(" and ");
80+
};

0 commit comments

Comments
 (0)