Skip to content

Commit 4d1064b

Browse files
committed
progress
1 parent 2728b0c commit 4d1064b

File tree

14 files changed

+711
-23
lines changed

14 files changed

+711
-23
lines changed

frontend/src/components/body.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import { Dashboard } from '../pages/dashboard/index.js'
33
import { ExportStack } from '../pages/import-export/export-stack.js'
44
import { ImportStack } from '../pages/import-export/import-stack.js'
55
import { Init, Offline } from '../pages/index.js'
6+
import { CreateRepository } from '../pages/repositories/create-repository.js'
7+
import { EditRepository } from '../pages/repositories/edit-repository.js'
68
import { CreateService } from '../pages/services/create-service.js'
79
import { ServiceDetail } from '../pages/services/service-detail.js'
810
import { ServiceLogs } from '../pages/services/service-logs.js'
911
import { UserSettings } from '../pages/settings/user-settings.js'
1012
import { CreateStack } from '../pages/stacks/create-stack.js'
13+
import { EditStack } from '../pages/stacks/edit-stack.js'
1114
import { SessionService } from '../services/session.js'
1215

1316
const appRoutes = {
@@ -23,6 +26,12 @@ const appRoutes = {
2326
'/services/:id': {
2427
component: ({ match }) => <ServiceDetail serviceId={match.params.id} />,
2528
},
29+
'/repositories/create/:stackName': {
30+
component: ({ match }) => <CreateRepository stackName={match.params.stackName} />,
31+
},
32+
'/repositories/:id': {
33+
component: ({ match }) => <EditRepository repositoryId={match.params.id} />,
34+
},
2635
'/settings': {
2736
component: () => <UserSettings />,
2837
},
@@ -32,6 +41,9 @@ const appRoutes = {
3241
'/stacks/import': {
3342
component: () => <ImportStack />,
3443
},
44+
'/stacks/:name/edit': {
45+
component: ({ match }) => <EditStack stackName={match.params.name} />,
46+
},
3547
'/stacks/:name/export': {
3648
component: ({ match }) => <ExportStack stackName={match.params.name} />,
3749
},
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { createComponent, Shade } from '@furystack/shades'
2+
import { Button, cssVariableTheme, Paper } from '@furystack/shades-common-components'
3+
4+
type ConfirmDialogProps = {
5+
title: string
6+
message: string
7+
confirmLabel?: string
8+
cancelLabel?: string
9+
variant?: 'danger' | 'default'
10+
onConfirm: () => void
11+
onCancel: () => void
12+
}
13+
14+
export const ConfirmDialog = Shade<ConfirmDialogProps>({
15+
shadowDomName: 'shade-confirm-dialog',
16+
render: ({ props }) => {
17+
const confirmLabel = props.confirmLabel ?? 'Confirm'
18+
const cancelLabel = props.cancelLabel ?? 'Cancel'
19+
const isDanger = props.variant === 'danger'
20+
21+
return (
22+
<div
23+
style={{
24+
position: 'fixed',
25+
inset: '0',
26+
zIndex: '10000',
27+
display: 'flex',
28+
alignItems: 'center',
29+
justifyContent: 'center',
30+
background: 'rgba(0, 0, 0, 0.6)',
31+
}}
32+
onclick={(ev) => {
33+
if (ev.target === ev.currentTarget) {
34+
props.onCancel()
35+
}
36+
}}
37+
>
38+
<Paper
39+
elevation={3}
40+
style={{
41+
padding: '24px',
42+
minWidth: '360px',
43+
maxWidth: '480px',
44+
borderRadius: '12px',
45+
background: cssVariableTheme.background.paper,
46+
}}
47+
>
48+
<h3 style={{ margin: '0 0 12px 0', fontSize: '18px' }}>{props.title}</h3>
49+
<p style={{ margin: '0 0 24px 0', opacity: '0.8', fontSize: '14px', lineHeight: '1.5' }}>{props.message}</p>
50+
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
51+
<Button variant="outlined" onclick={props.onCancel}>
52+
{cancelLabel}
53+
</Button>
54+
<Button variant="contained" color={isDanger ? 'error' : 'primary'} onclick={props.onConfirm}>
55+
{confirmLabel}
56+
</Button>
57+
</div>
58+
</Paper>
59+
</div>
60+
)
61+
},
62+
})

frontend/src/components/entity-forms/service-form.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { createComponent, Shade } from '@furystack/shades'
2-
import { Button, Input } from '@furystack/shades-common-components'
3-
import type { Service } from 'common'
2+
import { Button, Input, Select } from '@furystack/shades-common-components'
3+
import type { GitHubRepository, Service } from 'common'
44

55
type ServiceFormProps = {
66
initial?: Partial<Service>
77
stackName: string
8+
repositories?: GitHubRepository[]
89
onSubmit: (data: Partial<Service>) => void | Promise<void>
910
onCancel: () => void
1011
mode: 'create' | 'edit'
@@ -25,6 +26,7 @@ export const ServiceForm = Shade<ServiceFormProps>({
2526
displayName: data.displayName,
2627
description: data.description,
2728
workingDirectory: data.workingDirectory,
29+
repositoryId: data.repositoryId || undefined,
2830
runCommand: data.runCommand,
2931
installCommand: data.installCommand || undefined,
3032
buildCommand: data.buildCommand || undefined,
@@ -48,6 +50,20 @@ export const ServiceForm = Shade<ServiceFormProps>({
4850
variant="outlined"
4951
value={props.initial?.description ?? ''}
5052
/>
53+
{props.repositories && props.repositories.length > 0 ? (
54+
<Select
55+
name="repositoryId"
56+
labelTitle="GitHub Repository"
57+
variant="outlined"
58+
placeholder="None"
59+
value={props.initial?.repositoryId ?? ''}
60+
options={[
61+
{ value: '', label: '(None)' },
62+
...props.repositories.map((r) => ({ value: r.id, label: r.displayName })),
63+
]}
64+
getHelperText={() => 'Link this service to a repository'}
65+
/>
66+
) : null}
5167
<Input
5268
name="workingDirectory"
5369
labelTitle="Working Directory"
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { createComponent, Shade } from '@furystack/shades'
2+
import { Button } from '@furystack/shades-common-components'
3+
import type { GitHubRepository } from 'common'
4+
5+
type RepositoryTableProps = {
6+
repositories: GitHubRepository[]
7+
onEdit: (repositoryId: string) => void
8+
}
9+
10+
export const RepositoryTable = Shade<RepositoryTableProps>({
11+
shadowDomName: 'shade-repository-table',
12+
css: {
13+
'& table': {
14+
width: '100%',
15+
borderCollapse: 'collapse',
16+
},
17+
'& th, & td': {
18+
textAlign: 'left',
19+
padding: '10px 12px',
20+
borderBottom: '1px solid rgba(255,255,255,0.08)',
21+
},
22+
'& th': {
23+
fontSize: '12px',
24+
textTransform: 'uppercase',
25+
letterSpacing: '0.5px',
26+
opacity: '0.7',
27+
},
28+
'& tr:hover td': {
29+
background: 'rgba(255,255,255,0.03)',
30+
},
31+
},
32+
render: ({ props }) => {
33+
return (
34+
<table>
35+
<thead>
36+
<tr>
37+
<th>Repository</th>
38+
<th>URL</th>
39+
<th style={{ width: '100px' }}>Actions</th>
40+
</tr>
41+
</thead>
42+
<tbody>
43+
{props.repositories.map((repo) => (
44+
<tr>
45+
<td>
46+
<strong>{repo.displayName}</strong>
47+
{repo.description ? (
48+
<div style={{ fontSize: '12px', opacity: '0.6', marginTop: '2px' }}>{repo.description}</div>
49+
) : null}
50+
</td>
51+
<td>
52+
<span style={{ fontFamily: 'monospace', fontSize: '13px' }}>{repo.url}</span>
53+
</td>
54+
<td>
55+
<Button variant="outlined" onclick={() => props.onEdit(repo.id)}>
56+
Edit
57+
</Button>
58+
</td>
59+
</tr>
60+
))}
61+
</tbody>
62+
</table>
63+
)
64+
},
65+
})

frontend/src/components/service-table.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ export const ServiceTable = Shade<ServiceTableProps>({
161161
<Button variant="outlined" onclick={() => props.onViewLogs(svc.id)}>
162162
Logs
163163
</Button>
164+
<Button variant="outlined" onclick={() => props.onEdit(svc.id)}>
165+
Edit
166+
</Button>
164167
</div>
165168
</td>
166169
</tr>

frontend/src/pages/dashboard/index.tsx

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useCollectionSync } from '@furystack/entity-sync-client'
22
import { createComponent, NestedRouteLink, Shade } from '@furystack/shades'
3-
import { Button, Loader, PageContainer, PageHeader, Paper } from '@furystack/shades-common-components'
4-
import { Service, Stack } from 'common'
3+
import { Button, Icon, icons, Loader, PageContainer, PageHeader, Paper } from '@furystack/shades-common-components'
4+
import { GitHubRepository, Service, Stack } from 'common'
55

6+
import { RepositoryTable } from '../../components/repository-table.js'
67
import { ServiceTable } from '../../components/service-table.js'
78
import { StackSelector } from '../../components/stack-selector.js'
89

@@ -24,6 +25,11 @@ export const Dashboard = Shade({
2425
})
2526
const services = servicesState.status === 'synced' || servicesState.status === 'cached' ? servicesState.data : []
2627

28+
const reposState = useCollectionSync(options, GitHubRepository, {
29+
filter: activeStackName ? { stackName: { $eq: activeStackName } } : undefined,
30+
})
31+
const repos = reposState.status === 'synced' || reposState.status === 'cached' ? reposState.data : []
32+
2733
const isLoading = stacksState.status === 'connecting'
2834

2935
if (isLoading) {
@@ -72,6 +78,15 @@ export const Dashboard = Shade({
7278
selectedStack={activeStackName}
7379
onSelect={(name) => setSelectedStackName(name)}
7480
/>
81+
{activeStackName ? (
82+
<Button
83+
variant="outlined"
84+
onclick={() => history.pushState(null, '', `/stacks/${activeStackName}/edit`)}
85+
startIcon={<Icon icon={icons.edit} size="small" />}
86+
>
87+
Edit Stack
88+
</Button>
89+
) : null}
7590
{activeStackName ? (
7691
<Button
7792
variant="contained"
@@ -84,6 +99,16 @@ export const Dashboard = Shade({
8499
}
85100
/>
86101
<Paper>
102+
<div
103+
style={{
104+
display: 'flex',
105+
justifyContent: 'space-between',
106+
alignItems: 'center',
107+
marginBottom: '12px',
108+
}}
109+
>
110+
<h3 style={{ margin: '0', fontSize: '16px' }}>Services</h3>
111+
</div>
87112
{services.length === 0 ? (
88113
<div style={{ textAlign: 'center', padding: '32px' }}>
89114
<p style={{ opacity: '0.5', marginBottom: '16px' }}>No services in this stack yet.</p>
@@ -104,6 +129,44 @@ export const Dashboard = Shade({
104129
/>
105130
)}
106131
</Paper>
132+
<Paper style={{ marginTop: '16px' }}>
133+
<div
134+
style={{
135+
display: 'flex',
136+
justifyContent: 'space-between',
137+
alignItems: 'center',
138+
marginBottom: '12px',
139+
}}
140+
>
141+
<h3 style={{ margin: '0', fontSize: '16px' }}>Repositories</h3>
142+
{activeStackName ? (
143+
<Button
144+
variant="outlined"
145+
onclick={() => history.pushState(null, '', `/repositories/create/${activeStackName}`)}
146+
>
147+
Add Repository
148+
</Button>
149+
) : null}
150+
</div>
151+
{repos.length === 0 ? (
152+
<div style={{ textAlign: 'center', padding: '32px' }}>
153+
<p style={{ opacity: '0.5', marginBottom: '16px' }}>No repositories in this stack yet.</p>
154+
{activeStackName ? (
155+
<Button
156+
variant="outlined"
157+
onclick={() => history.pushState(null, '', `/repositories/create/${activeStackName}`)}
158+
>
159+
Add Repository
160+
</Button>
161+
) : null}
162+
</div>
163+
) : (
164+
<RepositoryTable
165+
repositories={repos}
166+
onEdit={(repoId: string) => history.pushState(null, '', `/repositories/${repoId}`)}
167+
/>
168+
)}
169+
</Paper>
107170
</PageContainer>
108171
)
109172
},
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { createComponent, Shade } from '@furystack/shades'
2+
import { NotyService, PageContainer, PageHeader, Paper } from '@furystack/shades-common-components'
3+
import type { GitHubRepository } from 'common'
4+
5+
import { GitHubRepoForm } from '../../components/entity-forms/github-repo-form.js'
6+
import { GitHubReposApiClient } from '../../services/api-clients/github-repos-api-client.js'
7+
8+
type CreateRepositoryProps = {
9+
stackName: string
10+
}
11+
12+
export const CreateRepository = Shade<CreateRepositoryProps>({
13+
shadowDomName: 'shade-create-repository',
14+
render: ({ props, injector }) => {
15+
const handleSubmit = async (data: Partial<GitHubRepository>) => {
16+
try {
17+
await injector.getInstance(GitHubReposApiClient).call({
18+
method: 'POST',
19+
action: '/github-repositories',
20+
body: {
21+
id: crypto.randomUUID(),
22+
stackName: props.stackName,
23+
url: data.url!,
24+
displayName: data.displayName!,
25+
description: data.description ?? '',
26+
},
27+
})
28+
injector.getInstance(NotyService).emit('onNotyAdded', {
29+
title: 'Repository added',
30+
body: `"${data.displayName}" was added successfully.`,
31+
type: 'success',
32+
})
33+
history.pushState(null, '', '/')
34+
} catch (error) {
35+
injector.getInstance(NotyService).emit('onNotyAdded', {
36+
title: 'Error',
37+
body: error instanceof Error ? error.message : 'Failed to add repository',
38+
type: 'error',
39+
})
40+
}
41+
}
42+
43+
return (
44+
<PageContainer>
45+
<PageHeader icon="📦" title="Add Repository" description="Add a GitHub repository to this stack." />
46+
<Paper>
47+
<GitHubRepoForm
48+
mode="create"
49+
stackName={props.stackName}
50+
onSubmit={(data) => void handleSubmit(data)}
51+
onCancel={() => history.pushState(null, '', '/')}
52+
/>
53+
</Paper>
54+
</PageContainer>
55+
)
56+
},
57+
})

0 commit comments

Comments
 (0)