Skip to content

Commit a727811

Browse files
author
Colin McNeil
committed
Finalize
1 parent 25180fa commit a727811

File tree

7 files changed

+230
-56
lines changed

7 files changed

+230
-56
lines changed

src/extension/Dockerfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,5 @@ COPY --from=builder /backend/bin/service /
3939
COPY docker-compose.yaml .
4040
COPY metadata.json .
4141
COPY docker.svg /docker.svg
42-
COPY --chmod=0755 binary/dist/install .
43-
COPY --chmod=0755 binary/dist/install.exe .
4442
COPY --from=client-builder /ui/build ui
4543
CMD /service -socket /run/guest-services/backend.sock

src/extension/metadata.json

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,5 @@
1212
"src": "index.html",
1313
"root": "ui"
1414
}
15-
},
16-
"host": {
17-
"binaries": [
18-
{
19-
"darwin": [
20-
{
21-
"path": "install"
22-
}
23-
],
24-
"linux": [
25-
{
26-
"path": "install"
27-
}
28-
],
29-
"windows": [
30-
{
31-
"path": "install.exe"
32-
}
33-
]
34-
}
35-
]
3615
}
3716
}

src/extension/ui/package-lock.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/extension/ui/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"@docker/extension-api-client": "0.3.4",
99
"@emotion/react": "11.10.4",
1010
"@emotion/styled": "11.10.4",
11+
"@mui/icons-material": "^5.16.5",
1112
"@mui/material": "5.10.8",
1213
"react": "^18.2.0",
1314
"react-dom": "^18.2.0"
@@ -28,4 +29,4 @@
2829
"typescript": "^4.8.3",
2930
"vite": "^3.1.0"
3031
}
31-
}
32+
}

src/extension/ui/src/App.tsx

Lines changed: 165 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
11
import React, { useEffect } from 'react';
22
import Button from '@mui/material/Button';
3+
import DelIcon from '@mui/icons-material/Delete';
34
import { createDockerDesktopClient } from '@docker/extension-api-client';
4-
import { Divider, FormControlLabel, FormGroup, Grid, Link, List, ListItem, Modal, Paper, Stack, Switch, TextField, Tooltip, Typography } from '@mui/material';
5-
import { title } from 'process';
5+
import { Divider, FormControlLabel, FormGroup, Grid, Icon, IconButton, Link, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Modal, Paper, Stack, Switch, TextField, Tooltip, Typography } from '@mui/material';
6+
import { getRunArgs } from './args';
7+
import { on } from 'events';
68

79
// Note: This line relies on Docker Desktop's presence as a host application.
810
// If you're running this React app in a browser, it won't work properly.
911
const client = createDockerDesktopClient();
1012

13+
const debounce = (fn: Function, ms: number) => {
14+
let timeout: NodeJS.Timeout;
15+
return function (...args: any) {
16+
clearTimeout(timeout);
17+
timeout = setTimeout(() => fn(...args), ms);
18+
};
19+
}
20+
21+
const debouncedToastSuccess = debounce(client.desktopUI.toast.success, 1000)
22+
1123
export function App() {
1224
const [projects, setProjects] = React.useState<string[]>(localStorage.getItem('projects') ? JSON.parse(localStorage.getItem('projects')!) : []);
1325
const [selectedProject, setSelectedProject] = React.useState<string | null>(null);
1426

1527
const [prompts, setPrompts] = React.useState<string[]>(localStorage.getItem('prompts') ? JSON.parse(localStorage.getItem('prompts')!) : []);
1628
const [selectedPrompt, setSelectedPrompt] = React.useState<string | null>(null);
1729

30+
const [openAIKey, setOpenAIKey] = React.useState<string | null>(null);
31+
32+
const [promptInput, setPromptInput] = React.useState<string>('');
33+
34+
const [runOut, setRunOut] = React.useState<string>('');
35+
1836
useEffect(() => {
1937
localStorage.setItem('projects', JSON.stringify(projects));
2038
}, [projects]);
@@ -23,58 +41,173 @@ export function App() {
2341
localStorage.setItem('prompts', JSON.stringify(prompts));
2442
}, [prompts]);
2543

44+
useEffect(() => {
45+
debouncedToastSuccess('OpenAI key saved');
46+
localStorage.setItem('openAIKey', openAIKey || '');
47+
}, [openAIKey]);
48+
49+
50+
useEffect(() => {
51+
// URL format: https://github.com/<owner>/<repo>/tree/<branch>/<path>
52+
// REF format: github.com:<owner>/<repo>?ref=<branch>&path=<path>
53+
if (promptInput?.startsWith('http')) {
54+
// Convert URL to REF
55+
const url = new URL(promptInput);
56+
const registry = url.hostname
57+
const owner = url.pathname.split('/')[1];
58+
const repo = url.pathname.split('/')[2];
59+
const branch = url.pathname.split('/')[4];
60+
const path = url.pathname.split('/').slice(5).join('/');
61+
const ref = `${registry}:${owner}/${repo}?ref=${branch}&path=${path}`;
62+
setPromptInput(ref);
63+
}
64+
}, [promptInput]);
65+
66+
const delim = client.host.platform === 'win32' ? '\\' : '/';
67+
2668
return (
2769
<>
2870
<Grid container spacing={2}>
71+
<Grid item xs={12}>
72+
<Paper sx={{ padding: 1, pl: 2 }}>
73+
<Stack direction='row' spacing={2} alignItems={'center'} justifyContent={'space-between'}>
74+
<Typography sx={{ flex: '1 1 30%' }} variant='h4'>OpenAI Key</Typography>
75+
<TextField sx={{ flex: '1 1 70%' }} onChange={e => setOpenAIKey(e.target.value)} value={openAIKey} placeholder='Enter OpenAI API key' type='password' />
76+
</Stack>
77+
</Paper>
78+
</Grid>
2979
{/* Projects column */}
30-
<Grid item xs={4}>
31-
<Paper>
32-
<Typography variant="h6" component="h2">Projects</Typography>
80+
<Grid item xs={6}>
81+
<Paper sx={{ padding: 2 }}>
82+
<Stack direction='row' spacing={2} alignItems={'center'} justifyContent={'space-between'}>
83+
<Typography variant='h2' sx={{ m: 2, display: 'inline' }}>Projects</Typography>
84+
<Button sx={{ padding: 1 }} onClick={() => {
85+
client.desktopUI.dialog.showOpenDialog({
86+
properties: ['openDirectory', 'multiSelections']
87+
}).then((result) => {
88+
if (result.canceled) {
89+
return;
90+
}
91+
const newProjects = result.filePaths
92+
setProjects([...projects, ...newProjects]);
93+
});
94+
}}>
95+
Add project
96+
</Button>
97+
</Stack>
3398
<List>
3499
{projects.map((project) => (
35-
<ListItem key={project}>
36-
<Button onClick={() => setSelectedProject(project)}>{project}</Button>
100+
<ListItem
101+
sx={theme => ({ borderLeft: 'solid black 3px', borderColor: selectedProject === project ? theme.palette.success.main : 'none', my: 0.5, padding: 0 })}
102+
secondaryAction={
103+
<IconButton color='error' onClick={() => {
104+
// Confirm
105+
const confirm = window.confirm(`Are you sure you want to remove ${project}?`);
106+
if (!confirm) {
107+
return;
108+
}
109+
setProjects(projects.filter((p) => p !== project));
110+
}}>
111+
<DelIcon />
112+
</IconButton>
113+
}>
114+
<ListItemButton sx={{ padding: 0, pl: 1.5 }} onClick={() => {
115+
setSelectedProject(project);
116+
}}>
117+
<ListItemText primary={project.split(delim).pop()} secondary={project} />
118+
</ListItemButton>
37119
</ListItem>
38120
))}
39121
</List>
40-
<Button onClick={() => {
41-
const newProject = prompt('Enter the name of the project');
42-
if (newProject) {
43-
setProjects([...projects, newProject]);
44-
}
45-
}}>Add project</Button>
46122
</Paper>
47123
</Grid>
48-
49124
{/* Prompts column */}
50-
<Grid item xs={4}>
51-
<Paper>
52-
<Typography variant="h6" component="h2">Prompts</Typography>
125+
<Grid item xs={6}>
126+
<Paper sx={{ padding: 2 }}>
127+
<Typography variant="h2" component="h2">Prompts</Typography>
128+
<TextField
129+
sx={{ width: '100%' }}
130+
placeholder='Enter GitHub ref or URL'
131+
value={promptInput}
132+
onChange={(e) => setPromptInput(e.target.value)}
133+
/>
134+
{promptInput.length > 0 && (
135+
<Button onClick={() => {
136+
setPrompts([...prompts, promptInput]);
137+
setPromptInput('');
138+
}}>Add prompt</Button>
139+
)}
53140
<List>
54141
{prompts.map((prompt) => (
55-
<ListItem key={prompt}>
56-
<Button onClick={() => setSelectedPrompt(prompt)}>{prompt}</Button>
142+
<ListItem
143+
sx={theme => ({
144+
borderLeft: 'solid black 3px',
145+
borderColor: selectedPrompt === prompt ? theme.palette.success.main : 'none',
146+
my: 0.5,
147+
padding: 0
148+
})}
149+
secondaryAction={
150+
<IconButton color='error' onClick={() => {
151+
// Confirm
152+
const confirm = window.confirm(`Are you sure you want to remove ${prompt}?`);
153+
if (!confirm) {
154+
return;
155+
}
156+
setPrompts(prompts.filter((p) => p !== prompt));
157+
}}>
158+
<DelIcon />
159+
</IconButton>
160+
}>
161+
<ListItemButton sx={{ padding: 0, pl: 1.5 }} onClick={() => {
162+
setSelectedPrompt(prompt);
163+
}}>
164+
<ListItemText primary={prompt.split(delim).pop()} secondary={prompt} />
165+
</ListItemButton>
57166
</ListItem>
58167
))}
59168
</List>
60-
<Button onClick={() => {
61-
const newPrompt = prompt('Enter the name of the prompt');
62-
if (newPrompt) {
63-
setPrompts([...prompts, newPrompt]);
64-
}
65-
}}>Add prompt</Button>
66169
</Paper>
67170
</Grid>
68171
{/* Show row at bottom if selectProject AND selectedPrompt */}
69-
{selectedProject && selectedPrompt && (
172+
{selectedProject && selectedPrompt ? (
173+
<Grid item xs={12}>
174+
<Paper sx={{ padding: 1 }}>
175+
<Typography variant="h3" component="h2">Ready</Typography>
176+
<Typography><pre>PROJECT={selectedProject}</pre></Typography>
177+
<Typography><pre>PROMPT={selectedPrompt}</pre></Typography>
178+
<Button sx={{ mt: 1, }} color='success' onClick={async () => {
179+
// Write openai key to $HOME/.openai-api-key
180+
setRunOut('Writing OpenAI key...');
181+
await client.extension.vm?.cli.exec('/bin/sh', ['-c', `echo ${openAIKey} > $HOME/.openai-api-key`]);
182+
setRunOut('Running...');
183+
client.docker.cli.exec('run', getRunArgs(selectedPrompt, selectedProject, client.host.hostname, client.host.platform), {
184+
stream: {
185+
onError: (err) => {
186+
setRunOut(runOut + '\n' + err.message);
187+
},
188+
onOutput: ({ stdout, stderr }) => {
189+
setRunOut(runOut + '\n' + (stdout || stderr));
190+
}
191+
}
192+
})
193+
}}>
194+
<Typography variant='h3'>Run</Typography>
195+
</Button>
196+
</Paper>
197+
</Grid>
198+
) : (
70199
<Grid item xs={12}>
71200
<Paper>
72-
<Typography variant="h6" component="h2">Selected</Typography>
73-
<Typography>Project: {selectedProject}</Typography>
74-
<Typography>Prompt: {selectedPrompt}</Typography>
75-
<Button onClick={() => {
76-
client.docker.cli.exec('docker', ['run'])
77-
}}>Run {selectedPrompt} in {selectedProject}</Button>
201+
You must select a project and a prompt to run.
202+
</Paper>
203+
</Grid>
204+
)}
205+
{/* Show run output */}
206+
{runOut && (
207+
<Grid item xs={12}>
208+
<Paper>
209+
<Typography variant='h3'>Run output</Typography>
210+
<pre>{runOut}</pre>
78211
</Paper>
79212
</Grid>
80213
)}

src/extension/ui/src/args.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export const getRunArgs = (prompt_ref: string, project_dir: string, username: string, platform: string) => {
2+
// docker run --rm \
3+
// -it \
4+
// -v /var/run/docker.sock:/var/run/docker.sock \
5+
// --mount type=volume,source=docker-prompts,target=/prompts \
6+
// --mount type=bind,source=$HOME/.openai-api-key,target=/root/.openai-api-key \
7+
// vonwig/prompts:latest \
8+
// run \
9+
// $PWD \
10+
// $USER \
11+
// "$(uname -o)" \
12+
// "github:docker/labs-githooks?ref=main&path=prompts/git_hooks"
13+
return [
14+
'--rm',
15+
'-it',
16+
'-v',
17+
'/var/run/docker.sock:/var/run/docker.sock',
18+
'--mount',
19+
'type=volume,source=docker-prompts,target=/prompts',
20+
'--mount',
21+
'type=bind,source=$HOME/.openai-api-key,target=/root/.openai-api-key',
22+
'vonwig/prompts:latest',
23+
'run',
24+
project_dir,
25+
username,
26+
platform,
27+
prompt_ref
28+
];
29+
30+
}

src/extension/ui/yarn.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,13 @@
727727
resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.17.tgz"
728728
integrity sha512-DVAejDQkjNnIac7MfP8sLzuo7fyrBPxNdXe+6bYqOqg1z2OPTlfFAejSNzWe7UenRMuFu9/AyFXj/X2vN2w6dA==
729729

730+
"@mui/icons-material@^5.16.5":
731+
version "5.16.5"
732+
resolved "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.5.tgz"
733+
integrity sha512-bn88xxU/J9UV0s6+eutq7o3TTOrOlbCX+KshFb8kxgIxJZZfYz3JbAXVMivvoMF4Md6jCVUzM9HEkf4Ajab4tw==
734+
dependencies:
735+
"@babel/runtime" "^7.23.9"
736+
730737
"@mui/material@^5.0.0", "@mui/[email protected]":
731738
version "5.10.8"
732739
resolved "https://registry.npmjs.org/@mui/material/-/material-5.10.8.tgz"

0 commit comments

Comments
 (0)