Skip to content

Commit 738fabe

Browse files
committed
feat(project): Enhance ProjectPage with loading state and deployment selection; refactor DeploymentPage and update SettingsPage and TasksPage to use project data
1 parent 5f410b5 commit 738fabe

11 files changed

Lines changed: 747 additions & 73 deletions

File tree

api/routes.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ const defs = {
138138
authorize: withAdminSession,
139139
fn: (_ctx, project) => ProjectsCollection.insert(project),
140140
input: OBJ({
141-
projectSlug: STR('The unique identifier for the project'),
142-
projectName: STR('The name of the project'),
141+
slug: STR('The unique identifier for the project'),
142+
name: STR('The name of the project'),
143143
teamId: STR('The ID of the team that owns the project'),
144144
isPublic: BOOL('Is the project public?'),
145145
repositoryUrl: optional(STR('The URL of the project repository')),
@@ -149,21 +149,21 @@ const defs = {
149149
}),
150150
'GET/api/project': route({
151151
authorize: withUserSession,
152-
fn: (_ctx, { projectSlug }) => {
153-
const project = ProjectsCollection.get(projectSlug)
152+
fn: (_ctx, { slug }) => {
153+
const project = ProjectsCollection.get(slug)
154154
if (!project) throw respond.NotFound({ message: 'Project not found' })
155155
return project
156156
},
157-
input: OBJ({ projectSlug: STR('The slug of the project') }),
157+
input: OBJ({ slug: STR('The slug of the project') }),
158158
output: ProjectDef,
159159
description: 'Get a project by ID',
160160
}),
161161
'PUT/api/project': route({
162162
authorize: withAdminSession,
163-
fn: (_ctx, input) => ProjectsCollection.update(input.projectSlug, input),
163+
fn: (_ctx, input) => ProjectsCollection.update(input.slug, input),
164164
input: OBJ({
165-
projectSlug: STR('The unique identifier for the project'),
166-
projectName: STR('The name of the project'),
165+
slug: STR('The unique identifier for the project'),
166+
name: STR('The name of the project'),
167167
teamId: STR('The ID of the team that owns the project'),
168168
isPublic: BOOL('Is the project public?'),
169169
repositoryUrl: optional(STR('The URL of the project repository')),
@@ -173,13 +173,13 @@ const defs = {
173173
}),
174174
'DELETE/api/project': route({
175175
authorize: withAdminSession,
176-
fn: (_ctx, { projectSlug }) => {
177-
const project = ProjectsCollection.get(projectSlug)
176+
fn: (_ctx, { slug }) => {
177+
const project = ProjectsCollection.get(slug)
178178
if (!project) throw respond.NotFound({ message: 'Project not found' })
179-
ProjectsCollection.delete(projectSlug)
179+
ProjectsCollection.delete(slug)
180180
return true
181181
},
182-
input: OBJ({ projectSlug: STR('The slug of the project') }),
182+
input: OBJ({ slug: STR('The slug of the project') }),
183183
output: BOOL('Indicates if the project was deleted'),
184184
description: 'Delete a project by ID',
185185
}),

api/schema.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,28 @@ export const TeamDef = OBJ({
2121
export type Team = Asserted<typeof TeamDef> & BaseRecord
2222

2323
export const ProjectDef = OBJ({
24-
projectSlug: STR('The unique identifier for the project'),
25-
projectName: STR('The name of the project'),
24+
slug: STR('The unique identifier for the project'),
25+
name: STR('The name of the project'),
2626
teamId: STR('The ID of the team that owns the project'),
2727
isPublic: BOOL('Is the project public?'),
2828
repositoryUrl: optional(STR('The URL of the project repository')),
2929
}, 'The project schema definition')
3030
export type Project = Asserted<typeof ProjectDef> & BaseRecord
3131

32+
export const DeploymentDef = OBJ({
33+
projectId: STR('The ID of the project this deployment belongs to'),
34+
url: STR('The URL of the deployment'),
35+
logsEnabled: BOOL('Are logs enabled for this deployment?'),
36+
clickhouseHost: optional(STR('The ClickHouse host for logs')),
37+
clickhousePort: optional(STR('The ClickHouse port for logs')),
38+
clickhouseUsername: optional(STR('The ClickHouse username for logs')),
39+
clickhousePassword: optional(STR('The ClickHouse password for logs')),
40+
databaseEnabled: BOOL('Is the database enabled for this deployment?'),
41+
sqlEndpoint: optional(STR('The SQL execution endpoint for the database')),
42+
sqlToken: optional(STR('The security token for the SQL endpoint')),
43+
}, 'The deployment schema definition')
44+
export type Deployment = Asserted<typeof DeploymentDef> & BaseRecord
45+
3246
export const UsersCollection = await createCollection<User, 'userEmail'>(
3347
{ name: 'users', primaryKey: 'userEmail' },
3448
)
@@ -39,7 +53,14 @@ export const TeamsCollection = await createCollection<Team, 'teamId'>(
3953

4054
export const ProjectsCollection = await createCollection<
4155
Project,
42-
'projectSlug'
56+
'slug'
57+
>(
58+
{ name: 'projects', primaryKey: 'slug' },
59+
)
60+
61+
export const DeploymentsCollection = await createCollection<
62+
Deployment,
63+
'url'
4364
>(
44-
{ name: 'projects', primaryKey: 'projectSlug' },
65+
{ name: 'deployments', primaryKey: 'url' },
4566
)

tasks/seed.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,22 @@ const teams: Team[] = [
4141

4242
const projects: Omit<Project, 'createdAt'>[] = [
4343
{
44-
projectSlug: 'website-redesign',
45-
projectName: 'Website Redesign',
44+
slug: 'website-redesign',
45+
name: 'Website Redesign',
4646
teamId: 'frontend-devs',
4747
isPublic: true,
4848
repositoryUrl: 'https://github.com/example/website',
4949
},
5050
{
51-
projectSlug: 'api-refactor',
52-
projectName: 'API Refactor',
51+
slug: 'api-refactor',
52+
name: 'API Refactor',
5353
teamId: 'backend-devs',
5454
isPublic: false,
5555
repositoryUrl: 'https://github.com/example/api',
5656
},
5757
{
58-
projectSlug: 'design-system',
59-
projectName: 'Design System',
58+
slug: 'design-system',
59+
name: 'Design System',
6060
teamId: 'frontend-devs',
6161
isPublic: true,
6262
repositoryUrl: 'https://github.com/example/design-system',

web/components/Layout.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,15 @@ export const PageLayout = (
2323
)
2424

2525
export const PageHeader = (
26-
{ children }: { children: JSX.Element | JSX.Element[] },
26+
{ children, className }: {
27+
className?: string
28+
children: JSX.Element | JSX.Element[]
29+
},
2730
) => (
28-
<header class='px-4 sm:px-6 py-4 bg-surface border-b border-divider'>
31+
<header
32+
class={['px-4 sm:px-6 py-4 bg-surface border-b border-divider', className]
33+
.join(' ')}
34+
>
2935
<div class='flex flex-col lg:flex-row justify-between items-start lg:items-center gap-3 sm:gap-4'>
3036
{children}
3137
</div>

web/components/SideBar.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Settings,
88
} from 'lucide-preact'
99
import { A, LinkProps, url } from '../lib/router.tsx'
10+
import { user } from '../lib/session.ts'
1011

1112
const NavLink = (
1213
{ icon: Icon, children, current, ...props }: LinkProps & {
@@ -21,6 +22,7 @@ const NavLink = (
2122
? 'bg-primary/10 text-primary'
2223
: 'text-base-content/70 hover:bg-base-300'
2324
} ${url.params.sidebar_collapsed === 'true' ? 'justify-center' : ''}`}
25+
replace
2426
>
2527
<Icon class='h-5 w-5' />
2628
{url.params.sidebar_collapsed !== 'true' && <span>{children}</span>}
@@ -70,10 +72,16 @@ export const SideBar = () => {
7072
Tasks
7173
</NavLink>
7274
</nav>
73-
<div class='border-t border-base-300 p-4'>
75+
<div
76+
class={`border-t border-base-300 p-4 ${
77+
user.data?.isAdmin ? '' : 'opacity-50 pointer-events-none'
78+
}`}
79+
>
7480
<NavLink
7581
current={nav === 'settings'}
76-
params={{ ...url.params, nav: 'settings' }}
82+
{...user.data?.isAdmin
83+
? { params: { ...url.params, nav: 'settings' } }
84+
: {}}
7785
icon={Settings}
7886
>
7987
Settings

web/components/forms.tsx

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { JSX } from 'preact'
2+
import { useId } from 'preact/hooks'
3+
import { A, LinkProps } from '../lib/router.tsx'
4+
5+
// Card component
6+
export const Card = (
7+
{ children, title, description }: {
8+
title: string
9+
description?: string
10+
children: JSX.Element | JSX.Element[]
11+
},
12+
) => (
13+
<div class='bg-surface rounded-lg border border-divider shadow-sm bg-base-100'>
14+
<div class='p-4 sm:p-6'>
15+
<h3 class='text-lg font-semibold text-text'>{title}</h3>
16+
{description && (
17+
<p class='mt-1 text-sm text-text-secondary'>{description}</p>
18+
)}
19+
</div>
20+
<div class='p-4 sm:p-6 border-t border-divider'>
21+
{children}
22+
</div>
23+
</div>
24+
)
25+
26+
// Input component
27+
export const Input = (
28+
{ label, name, note, ...props }:
29+
& { label: string; name: string; note?: string }
30+
& JSX.InputHTMLAttributes<HTMLInputElement>,
31+
) => {
32+
const id = useId()
33+
return (
34+
<div>
35+
<label for={id} class='block text-sm font-medium text-text-secondary'>
36+
{label}
37+
</label>
38+
<input
39+
id={id}
40+
name={name}
41+
class='mt-1 block w-full px-3 py-2 bg-bg border border-divider rounded-md shadow-sm placeholder-text-disabled focus:outline-none focus:ring-primary focus:border-primary sm:text-sm'
42+
{...props}
43+
/>
44+
{note && <p class='mt-2 text-sm text-text-secondary'>{note}</p>}
45+
</div>
46+
)
47+
}
48+
49+
// Button component
50+
export const Button = (
51+
{ children, variant = 'primary', ...props }:
52+
& {
53+
variant?: 'primary' | 'secondary' | 'danger'
54+
}
55+
& Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, 'class' | 'style'>
56+
& Partial<LinkProps>,
57+
) => {
58+
const baseClasses =
59+
'inline-flex justify-center py-2 px-4 border shadow-sm text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2'
60+
61+
const variants = {
62+
primary:
63+
'border-transparent text-white bg-primary hover:bg-primary-hover focus:ring-primary',
64+
secondary:
65+
'border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-indigo-500',
66+
danger:
67+
'border-transparent text-white bg-red-600 hover:bg-red-700 focus:ring-red-500',
68+
}
69+
70+
if (props.href || props.hash || props.params) {
71+
return (
72+
<A
73+
class={[baseClasses, variants[variant]].join(' ')}
74+
{...props}
75+
>
76+
{children}
77+
</A>
78+
)
79+
}
80+
81+
return (
82+
<button
83+
class={[baseClasses, variants[variant]].join(' ')}
84+
{...props}
85+
>
86+
{children}
87+
</button>
88+
)
89+
}
90+
91+
// Switch component
92+
export const Switch = (
93+
{ label, note, ...props }: {
94+
label: string
95+
note?: string
96+
// checked: boolean
97+
} & JSX.InputHTMLAttributes<HTMLInputElement>,
98+
) => {
99+
const id = useId()
100+
return (
101+
<div class='flex items-center justify-between'>
102+
<span class='flex-grow flex flex-col'>
103+
<label for={id} class='text-sm font-medium text-text-secondary'>
104+
{label}
105+
</label>
106+
{note && <p class='text-sm text-text-tertiary'>{note}</p>}
107+
</span>
108+
<div class='relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in'>
109+
<input
110+
type='checkbox'
111+
id={id}
112+
class='absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-green-400'
113+
{...props}
114+
/>
115+
<label
116+
for={id}
117+
class='block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer'
118+
>
119+
</label>
120+
</div>
121+
</div>
122+
)
123+
}
124+
125+
// Note component
126+
export const Note = ({ children }: { children: string }) => (
127+
<p class='mt-2 text-sm text-text-secondary'>{children}</p>
128+
)

0 commit comments

Comments
 (0)