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

Commit d71000f

Browse files
committed
feat(): huge refactor to db connection, add edit
1 parent 2a6873d commit d71000f

13 files changed

+330
-67
lines changed

ui/src/components/CardMenu.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import { Divider } from "@mui/material";
77
import { Delete, Edit, Stop } from "@mui/icons-material";
88
import EmptyConfirmationDialog from "./DeleteDBDialog";
99
import { IDBConnection } from "../utils/types";
10+
import { EditDatabaseDialog } from "./EditConnection";
1011

1112
const ITEM_HEIGHT = 48;
1213

1314
export function CardMenu({ database }: { database: IDBConnection }) {
1415
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
1516
const [dialogOpen, setDialogOpen] = React.useState(false);
17+
const [editOpen, setEditOpen] = React.useState(false);
1618
const open = Boolean(anchorEl);
1719
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
1820
setAnchorEl(event.currentTarget);
@@ -50,7 +52,7 @@ export function CardMenu({ database }: { database: IDBConnection }) {
5052
},
5153
}}
5254
>
53-
<MenuItem key={"Edit Database"} onClick={handleClose}>
55+
<MenuItem key={"Edit Database"} onClick={() => setEditOpen(true)}>
5456
<Edit fontSize="small" sx={{ marginRight: 1 }} />
5557
{"Edit Database"}
5658
</MenuItem>
@@ -65,7 +67,18 @@ export function CardMenu({ database }: { database: IDBConnection }) {
6567
</MenuItem>
6668
</Menu>
6769
</div>
68-
<EmptyConfirmationDialog open={dialogOpen} containerName={database.name} onClose={() => setDialogOpen(false)} />
70+
<EmptyConfirmationDialog
71+
open={dialogOpen}
72+
containerName={database.containerName}
73+
onClose={() => setDialogOpen(false)}
74+
/>
75+
{editOpen && (
76+
<EditDatabaseDialog
77+
database={database}
78+
open={editOpen}
79+
onClose={() => setEditOpen(false)}
80+
/>
81+
)}
6982
</>
7083
);
7184
}

ui/src/components/DatabaseCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useTestConnection } from "../hooks/useTestConnection";
77
import { IDBConnection } from "../utils/types";
88
import { CardMenu } from "./CardMenu";
99
import DatabaseViewerDialog from "./DatabaseViewerDialog";
10+
import { EditDatabaseDialog } from "./EditConnection";
1011

1112
const Transition = React.forwardRef(function Transition(
1213
props: TransitionProps & {
@@ -76,10 +77,10 @@ export const DatabaseCard = ({ database }: { database: IDBConnection }) => {
7677
variant="h3"
7778
onClick={isConnected ? handleClickOpen : () => {}}
7879
sx={{
79-
cursor: isConnected ? "pointer" : "",
80+
cursor: isConnected ? "pointer" : "not-allowed",
8081
}}
8182
>
82-
{database.name}
83+
{database.containerName}
8384
</Typography>
8485
</CardContent>
8586
</Card>

ui/src/components/DatabaseViewerDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export default function DatabaseViewerDialog(props: Props) {
112112
<ArrowBack />
113113
</IconButton>
114114
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
115-
{database.name}
115+
{database.containerName}
116116
</Typography>
117117
<Box sx={{ flexGrow: 1 }} />
118118
<IconButton

ui/src/components/DatabasesCards.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const DatabasesCards = () => {
2929
paddingX: { xs: 2, sm: 4, md: 8, lg: 12, xl: 16}
3030
}}>
3131
{databases.map((database) => (
32-
<DatabaseCard key={database.id} database={database} />
32+
<DatabaseCard key={database.containerId} database={database} />
3333
))}
3434
<AddNewDatabaseCard />
3535
{ currentDatabase && (
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { createDockerDesktopClient } from "@docker/extension-api-client";
2+
import { ExecResult } from "@docker/extension-api-client-types/dist/v1";
3+
import {
4+
Box,
5+
Button,
6+
CircularProgress,
7+
Dialog,
8+
DialogActions,
9+
DialogContent,
10+
DialogProps,
11+
DialogTitle,
12+
MenuItem,
13+
Stack,
14+
TextField,
15+
useMediaQuery,
16+
} from "@mui/material";
17+
import { Add } from "@mui/icons-material";
18+
import React from "react";
19+
import { useLocalStorage } from "../hooks/useLocalStorage";
20+
import { getConnectionString, officialDBs, setToLocalStorage } from "../utils";
21+
import { IConnection, IDBConnection } from "../utils/types";
22+
23+
const ddClient = createDockerDesktopClient();
24+
25+
interface EditDatabaseDialogProps extends DialogProps {
26+
database: IDBConnection;
27+
}
28+
29+
export const EditDatabaseDialog = (props: EditDatabaseDialogProps) => {
30+
// TODO: remove this as soon as the icon button colors are fixed in the design system
31+
const useDarkTheme = useMediaQuery("(prefers-color-scheme: dark)");
32+
33+
const { onClose, ...other } = props;
34+
35+
const [username, setUsername] = React.useState(props.database.connection.username);
36+
const [password, setPassword] = React.useState(props.database.connection.password);
37+
const [port, setPort] = React.useState(props.database.connection.port);
38+
const [database, setDatabase] = React.useState(props.database.connection.database);
39+
const [creatingContainer, setCreatingContainer] = React.useState(false);
40+
const [isValid, setIsValid] = React.useState(false);
41+
const [error, setError] = React.useState("");
42+
43+
const resetForm = () => {
44+
setUsername("");
45+
setPassword("");
46+
setPort("");
47+
setDatabase("");
48+
};
49+
50+
const close = () => {
51+
resetForm();
52+
onClose?.({}, "escapeKeyDown");
53+
};
54+
55+
const handleChangeUsername = (event: React.ChangeEvent<HTMLInputElement>) => {
56+
setUsername(event.target.value as string);
57+
setIsValid(Boolean(username && port && database));
58+
};
59+
60+
const handleChangePassword = (event: React.ChangeEvent<HTMLInputElement>) => {
61+
setPassword(event.target.value as string);
62+
};
63+
64+
const handleChangePort = (event: React.ChangeEvent<HTMLInputElement>) => {
65+
// TODO: check port availability
66+
setPort(event.target.value as string);
67+
setIsValid(Boolean(username && port && database));
68+
};
69+
70+
const handleChangeDatabase = (event: React.ChangeEvent<HTMLInputElement>) => {
71+
setDatabase(event.target.value as string);
72+
setIsValid(Boolean(username && port && database));
73+
};
74+
75+
const handleCreateDatabase = async () => {
76+
const foundDB = officialDBs.find(
77+
(db) => db.id === props.database.containerId
78+
);
79+
if (!foundDB) return;
80+
81+
const { image, defaults } = foundDB;
82+
83+
let envs = [];
84+
const values = { password, username, database };
85+
if (defaults.envs) {
86+
for (const key in defaults.envs) {
87+
const value = defaults.envs[key];
88+
const variableName = value.replaceAll("%", "");
89+
// @ts-expect-error type this
90+
const resolvedValue = values[variableName];
91+
envs.push("-e", `${key}=${resolvedValue}`);
92+
}
93+
}
94+
95+
setCreatingContainer(true);
96+
let containerID = "";
97+
try {
98+
const result = await ddClient.docker.cli.exec("run", [
99+
"-d",
100+
"-p",
101+
port,
102+
...envs,
103+
image,
104+
]);
105+
containerID = result.stdout.trim();
106+
} catch (e) {
107+
console.log(e);
108+
setCreatingContainer(false);
109+
setError((e as ExecResult).stderr);
110+
111+
return;
112+
}
113+
114+
const connectionString = getConnectionString(props.database.image, {
115+
password,
116+
username,
117+
database,
118+
port: port.split(":")[0],
119+
});
120+
121+
if (!connectionString) return;
122+
123+
let name = "";
124+
try {
125+
const result = await ddClient.docker.cli.exec("inspect", [
126+
"-f",
127+
"{{.Name}}",
128+
containerID,
129+
]);
130+
name = result.stdout.trim().replaceAll("/", "");
131+
} catch (e) {
132+
console.log(e);
133+
}
134+
135+
const dbConnection: IDBConnection = {
136+
containerId: containerID,
137+
containerName: name,
138+
connection: { port, username, password, database },
139+
image,
140+
};
141+
142+
setToLocalStorage(containerID, dbConnection.connection);
143+
144+
setCreatingContainer(false);
145+
// setCurrentDatabase(connection);
146+
close();
147+
};
148+
149+
const { fullWidth = true, ...rest } = props;
150+
return (
151+
<Dialog fullWidth={fullWidth} onClose={close} {...other}>
152+
<DialogTitle>Create a new database</DialogTitle>
153+
<DialogContent>
154+
<Stack gap={1.5} mt={2}>
155+
<TextField
156+
id="port"
157+
label="Port"
158+
value={port}
159+
onChange={handleChangePort}
160+
variant="outlined"
161+
/>
162+
<TextField
163+
id="database"
164+
label="Database name"
165+
value={database}
166+
onChange={handleChangeDatabase}
167+
variant="outlined"
168+
/>
169+
<TextField
170+
id="username"
171+
label="Username"
172+
value={username}
173+
onChange={handleChangeUsername}
174+
variant="outlined"
175+
/>
176+
<TextField
177+
id="password"
178+
type="password"
179+
label="Password"
180+
value={password}
181+
onChange={handleChangePassword}
182+
variant="outlined"
183+
/>
184+
{error && (
185+
<Box
186+
sx={{
187+
padding: 1,
188+
borderRadius: 1,
189+
backgroundColor: (theme) =>
190+
useDarkTheme
191+
? theme.palette.docker.red[200]
192+
: theme.palette.docker.red[100], // red-100 should be "#FDEAEA" but it's not 🤷‍♂️
193+
}}
194+
>
195+
{error}
196+
</Box>
197+
)}
198+
</Stack>
199+
</DialogContent>
200+
<DialogActions>
201+
<Button autoFocus onClick={close} disabled={creatingContainer}>
202+
Cancel
203+
</Button>
204+
<Button
205+
onClick={handleCreateDatabase}
206+
disabled={creatingContainer || !isValid}
207+
startIcon={
208+
creatingContainer ? <CircularProgress size={20} /> : <Add />
209+
}
210+
>
211+
Create
212+
</Button>
213+
</DialogActions>
214+
</Dialog>
215+
);
216+
};

ui/src/components/NewDatabaseDialog.tsx

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,15 @@ import {
99
DialogContent,
1010
DialogProps,
1111
DialogTitle,
12-
FormControl,
13-
InputLabel,
1412
MenuItem,
15-
Select,
16-
SelectChangeEvent,
1713
Stack,
1814
TextField,
1915
useMediaQuery,
2016
} from "@mui/material";
2117
import { Add } from "@mui/icons-material";
22-
import React, { useCallback } from "react";
23-
import { useCurrentDatabaseContext } from "../CurrentDatabaseContext";
18+
import React from "react";
2419
import { useLocalStorage } from "../hooks/useLocalStorage";
25-
import { getConnectionString, officialDBs } from "../utils";
20+
import { officialDBs, setToLocalStorage } from "../utils";
2621
import { IConnection, IDBConnection } from "../utils/types";
2722

2823
const ddClient = createDockerDesktopClient();
@@ -33,8 +28,7 @@ export const NewDatabaseDialog = (props: DialogProps) => {
3328

3429
const { onClose, ...other } = props;
3530

36-
const [_, setValue] = useLocalStorage("connexions", []);
37-
const { setDatabase: setCurrentDatabase } = useCurrentDatabaseContext();
31+
// const [_, setValue] = useLocalStorage("connexions", []);
3832
const [provider, setProvider] = React.useState("");
3933
const [username, setUsername] = React.useState("");
4034
const [password, setPassword] = React.useState("");
@@ -127,15 +121,6 @@ export const NewDatabaseDialog = (props: DialogProps) => {
127121
return;
128122
}
129123

130-
const connectionString = getConnectionString(provider, {
131-
password,
132-
username,
133-
database,
134-
port: port.split(":")[0],
135-
});
136-
137-
if (!connectionString) return;
138-
139124
let name = "";
140125
try {
141126
const result = await ddClient.docker.cli.exec("inspect", [
@@ -148,17 +133,17 @@ export const NewDatabaseDialog = (props: DialogProps) => {
148133
console.log(e);
149134
}
150135

151-
const connection: IDBConnection = {
152-
id: containerID,
153-
name,
154-
connectionString,
136+
const dbConnection: IDBConnection = {
137+
containerId: containerID,
138+
containerName: name,
139+
connection: { port, username, password, database },
155140
image,
156141
};
157142

158-
setValue((current: IConnection[]) => [...current, connection]);
143+
setToLocalStorage(containerID, dbConnection.connection);
159144

160145
setCreatingContainer(false);
161-
setCurrentDatabase(connection);
146+
// setCurrentDatabase(connection);
162147
close();
163148
};
164149

@@ -178,7 +163,18 @@ export const NewDatabaseDialog = (props: DialogProps) => {
178163
>
179164
{officialDBs.map((db, key) => (
180165
<MenuItem key={key} value={db.id}>
181-
{db.name}
166+
<Box sx={{
167+
display: "flex",
168+
alignItems: "center",
169+
gap: 1,
170+
}}>
171+
<img
172+
src={`https://raw.githubusercontent.com/docker/database-extension/main/ui/public/${db.image}.png`}
173+
alt={db.name}
174+
width={20}
175+
/>
176+
{db.name}
177+
</Box>
182178
</MenuItem>
183179
))}
184180
</TextField>

0 commit comments

Comments
 (0)