Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion shard-manager/src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,13 @@ const listSessions = t.procedure
z.array(
z.object({
projectId: zProjectId,
servers: z.array(z.object({ uuid: z.uuidv4(), viewer: z.object({ id: zUserId, name: zUserName }) })),
servers: z.array(
z.object({
uuid: z.uuidv4(),
viewer: z.object({ id: zUserId, name: zUserName }),
createdAt: z.iso.datetime(),
}),
),
}),
),
),
Expand Down
2 changes: 2 additions & 0 deletions shard-manager/src/vscodeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export class VscodeServerHandle implements AsyncDisposable {
readonly viewer
readonly owner
readonly project
readonly createdAt

constructor(viewer: BaseUser, owner: { id: string; name: string }, project: BaseProject) {
const { promise, resolve, reject } = Promise.withResolvers<void>()
Expand All @@ -131,6 +132,7 @@ export class VscodeServerHandle implements AsyncDisposable {
this.viewer = viewer
this.owner = owner
this.project = project
this.createdAt = new Date().toISOString()
}

/** Whether {@link start} has been called. */
Expand Down
10 changes: 5 additions & 5 deletions src/app/[userName]/ProjectRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export function ProjectRow({
return (
<>
<form action={renameAction} style={{ display: 'contents' }}>
<input type='hidden' name='projectId' value={project.id} />
<div className='info' style={{ flex: 1 }}>
<div style={{ overflowX: 'visible', textBox: 'normal' }}>
<input type='hidden' name='projectId' value={project.id} />
<input
name='name'
type='text'
Expand All @@ -47,7 +47,7 @@ export function ProjectRow({
if (e.key === 'Escape') setRenaming(false)
}}
placeholder='Project name'
style={{ width: '100%', marginBottom: 4 }}
style={{ width: '100%' }}
disabled={renamePending}
autoFocus
/>
Expand All @@ -60,8 +60,8 @@ export function ProjectRow({
Cancel
</button>
</div>
{renameError && <div style={{ color: '#dc2626', fontSize: 13 }}>{renameError}</div>}
</form>
{renameError && <div style={{ color: '#dc2626', fontSize: 13 }}>{renameError}</div>}
</>
)
}
Expand All @@ -71,7 +71,7 @@ export function ProjectRow({

return (
<>
<div className='info'>
<div>
<Link href={`/${username}/${encodeURIComponent(project.name)}/` as Route}>{project.name}</Link>
</div>
<div className='actions'>
Expand Down
4 changes: 2 additions & 2 deletions src/app/[userName]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ export default async function ProfileBody({ params: params_ }: { params: Promise
{isOwner ? <>No projects yet.{templates.length > 0 && ' Create one below.'}</> : 'No public projects.'}
</p>
) : (
<ul className='project-list'>
<ul className='action-list'>
{projects.map(p => (
<li key={p.id}>
{isOwner ? (
<ProjectRow project={p} username={user.name} canPublish={p.canPublish} />
) : (
<div className='info'>
<div>
<Link href={`/${user.name}/${encodeURIComponent(p.name)}/` as Route}>{p.name}</Link>
</div>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/app/admin/components/AllowlistEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ export function AllowlistEditor({ users }: { users: string[] }) {
{users.length === 0 ? (
<p className='empty'>No users in the allowlist.</p>
) : (
<ul className='project-list'>
<ul className='action-list'>
{users.map(u => (
<li key={u}>
<div className='info'>{u}</div>
<div>{u}</div>
<div className='actions'>
<button
className='delete'
Expand Down
40 changes: 31 additions & 9 deletions src/app/admin/components/SessionRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,39 @@ export function SessionRow({ info }: { info: EditorSessionInfo | UnknownEditorSe

return (
<li>
<div className='info'>
<a href={`/${info.viewerUsername}`}>{info.viewerUsername}</a>
<span style={{ color: '#90a4ae', margin: '0 0.25rem' }}>editing</span>
{'ownerUsername' in info ? (
<ProjectLink ownerUsername={info.ownerUsername} projectName={info.projectName} />
) : (
<div style={{ color: '#dc2626' }}>project {info.projectId} not listed in the database (deleted?)</div>
)}
<div>
<div>
{'ownerUsername' in info ? (
<ProjectLink ownerUsername={info.ownerUsername} projectName={info.projectName} />
) : (
<div style={{ color: '#dc2626' }}>Unlisted (deleted?) project {info.projectId}</div>
)}
</div>
<div className='subinfo'>
<a href={`/${info.viewerUsername}`}>{info.viewerUsername}</a>
<span> editing</span>
</div>
</div>
<div className='secondary'>
<div title={info.sessionId}>
<span>UUID {info.sessionId}</span>
</div>
<div className='subinfo'>
<span>
since{' '}
{info.createdAt.toLocaleString('en-us', {
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hourCycle: 'h23',
timeZone: 'UTC',
timeZoneName: 'short',
})}
</span>
</div>
</div>
<div className='actions'>
<span style={{ fontSize: '0.8rem', color: '#90a4ae' }}>UUID {info.sessionId}</span>
<button
className='delete'
disabled={killPending}
Expand Down
2 changes: 1 addition & 1 deletion src/app/admin/components/SessionViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async function SessionViewerData() {
return sessions.length === 0 ? (
<p className='empty'>No active editor sessions.</p>
) : (
<ul className='project-list'>
<ul className='action-list'>
{sessions.map(info => (
<SessionRow key={`${info.viewerId}/${info.projectId}`} info={info} />
))}
Expand Down
15 changes: 8 additions & 7 deletions src/app/admin/components/TemplateManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function TemplateManagement(props: TemplateManagementProps) {

function TemplateManagementList(props: { templates: TemplateInfo[] }) {
return (
<ul className='project-list'>
<ul className='action-list'>
{props.templates.map(template => (
<TemplateRow key={template.id} {...template} />
))}
Expand All @@ -61,17 +61,18 @@ function TemplateRow(props: TemplateInfo) {
const [isModalOpen, setIsModalOpen] = useState(false)

return (
<li style={{ display: 'grid', gridTemplateColumns: '1fr auto', width: '100%' }}>
<input type='hidden' name='id' value={props.id} />
<li>
<div>
<div style={{ fontSize: '13px' }}>
<div>
{props.name}
{!props.visible && <em> (hidden)</em>}
</div>
<div style={{ fontSize: '11px', color: '#90a4ae' }}>{props.description}</div>
{props.description && <div className='subinfo'>{props.description}</div>}
</div>
<div className='secondary' title={props.id}>
<span>ID {props.id}</span>
</div>
<div className='actions'>
<span style={{ fontSize: '0.8rem', color: '#90a4ae' }}>ID {props.id}</span>
<DialogTrigger isOpen={isModalOpen} onOpenChange={setIsModalOpen}>
<Button>Edit</Button>
<Modal isDismissable>
Expand Down Expand Up @@ -128,7 +129,7 @@ function TemplateEditForm(props: TemplateInfo & { onSuccess: () => void }) {
</Button>
</div>

<div role='alert' style={{ color: '#f00' }}>
<div role='alert' style={{ color: '#dc2626' }}>
{editError}
</div>
</form>
Expand Down
12 changes: 6 additions & 6 deletions src/app/admin/components/ToolchainManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function ToolchainManagementList(props: ToolchainManagementProps) {
const installedToolchains = use(props.installedToolchainsPromise)
if (installedToolchains.length === 0) return <p className='empty'>No installed toolchains.</p>
return (
<ul className='project-list'>
<ul className='action-list'>
{installedToolchains.map(toolchain => (
<ToolchainRow key={toolchain} toolchain={toolchain} />
))}
Expand All @@ -62,11 +62,11 @@ function ToolchainRow(props: { toolchain: string }) {
})
return (
<li>
<form className='simple-action-form' action={action}>
<div style={{ gridArea: 'name' }}>{props.toolchain}</div>
<div>{props.toolchain}</div>
<form className='actions' action={action}>
<input type='hidden' name='toolchain' value={props.toolchain} />
{EXPECTED_TOOLCHAIN_ID_RE.test(props.toolchain) /* prevent uninstall of weird-enough-named toolchains */ && (
<div className='actions' style={{ gridArea: 'actions' }}>
<>
{!confirm && (
<button type='button' onClick={() => setConfirm(true)}>
Remove Toolchain
Expand All @@ -82,10 +82,10 @@ function ToolchainRow(props: { toolchain: string }) {
</button>
</>
)}
</div>
</>
)}
<div style={{ gridArea: 'error', color: '#f00' }}>{error}</div>
</form>
<div style={{ color: '#dc2626' }}>{error}</div>
</li>
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/admin/components/UserManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function UserManagement() {
{users.length === 0 ? (
<p className='empty'>No users.</p>
) : (
<ul className='project-list'>
<ul className='action-list'>
{users.map(u => (
<UserRow key={u.id} user={u} isSelf={u.id === session.user.id} />
))}
Expand Down
38 changes: 18 additions & 20 deletions src/app/admin/components/UserRow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useRouter } from 'next/navigation'
import { startTransition, useState } from 'react'
import { startTransition } from 'react'

import { deleteUser, toggleAdmin } from '@/app/admin/actions'
import { useServerAction } from '@/lib/client/util'
Expand All @@ -10,7 +10,6 @@ type User = { id: string; name: string; isAdmin: boolean }

export function UserRow({ user, isSelf }: { user: User; isSelf: boolean }) {
const router = useRouter()
const [expanded, setExpanded] = useState(false)

const [toggleError, toggleAction, togglePending] = useServerAction(toggleAdmin, () => router.refresh())
const [deleteError, deleteAction, deletePending] = useServerAction(deleteUser, () => router.refresh())
Expand All @@ -32,25 +31,24 @@ export function UserRow({ user, isSelf }: { user: User; isSelf: boolean }) {
}

return (
<li style={{ display: 'block' }}>
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={() => setExpanded(x => !x)}>
<span style={{ width: 16, fontSize: '0.7rem', color: '#90a4ae' }}>{expanded ? '\u25BC' : '\u25B6'}</span>
<div className='info' style={{ flex: 1 }}>
<span>{user.name}</span>
{user.isAdmin && <span className='admin-badge'>admin</span>}
{isSelf && <span style={{ fontSize: '0.75rem', color: '#90a4ae', marginLeft: 8 }}>(you)</span>}
</div>
<li>
<div>
<span>{user.name}</span>
{isSelf && <span style={{ fontSize: '0.75rem', color: '#90a4ae', marginLeft: 8, marginRight: 8 }}>(you)</span>}
{user.isAdmin && <span className='admin-badge'>admin</span>}
</div>

<div className='secondary' title={user.id}>
ID {user.id}
</div>
<div className='actions'>
<button disabled={isSelf || pending} onClick={handleToggleAdmin}>
{user.isAdmin ? 'Make normal user' : 'Make admin'}
</button>
<button className='delete' disabled={isSelf || pending} onClick={handleDelete}>
Delete user
</button>
</div>
{expanded && (
<div style={{ padding: '12px 0 4px 16px', display: 'flex', gap: 8 }}>
<button disabled={isSelf || pending} onClick={handleToggleAdmin}>
{user.isAdmin ? 'Make normal user' : 'Make admin'}
</button>
<button className='delete' disabled={isSelf || pending} onClick={handleDelete}>
Delete user
</button>
</div>
)}
{error && <div style={{ color: '#dc2626', fontSize: 13 }}>{error}</div>}
</li>
)
Expand Down
Loading
Loading