Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions devbox-web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ WORKOS_COOKIE_PASSWORD=
WORKOS_REDIRECT_URI=http://localhost:3000/auth/callback
WORKOS_SIGN_OUT_REDIRECT_URI=http://localhost:3000/

# hosted-workos | local-dev-api | local-dev-fixtures
DEVBOX_DASHBOARD_DATA_MODE=local-dev-fixtures
DEVBOX_HOSTED_API_URL=
DEVBOX_LOCAL_API_URL=http://127.0.0.1:3001
DEVBOX_LOCAL_API_SESSION_TOKEN=
DEVBOX_LOCAL_API_DEVICE_ID=
44 changes: 44 additions & 0 deletions devbox-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,47 @@ WORKOS_COOKIE_PASSWORD=
WORKOS_REDIRECT_URI=http://localhost:3000/auth/callback
WORKOS_SIGN_OUT_REDIRECT_URI=http://localhost:3000/
```

The app owns these auth routes:

- `/auth/sign-in`
- `/auth/sign-up`
- `/auth/callback`
- `/auth/sign-out`

Configure the WorkOS sign-in endpoint as
`http://localhost:3000/auth/sign-in` and the redirect URI as
`http://localhost:3000/auth/callback`.

## Dashboard data

Authenticated dashboard routes are under `/dashboard`. The loaders require a
WorkOS session before reading any account, folder, or machine data.

Dashboard data modes:

```sh
DEVBOX_DASHBOARD_DATA_MODE=hosted-workos
DEVBOX_HOSTED_API_URL=https://api.example.com
```

Uses the WorkOS access token as the hosted API bearer token.

```sh
DEVBOX_DASHBOARD_DATA_MODE=local-dev-api
DEVBOX_LOCAL_API_URL=http://127.0.0.1:3001
DEVBOX_LOCAL_API_SESSION_TOKEN=
DEVBOX_LOCAL_API_DEVICE_ID=
```

Uses a real local `devbox-api` session token and device id. Create one with the
local API's `/v1/auth/dev-session` endpoint; do not put arbitrary account ids in
web app code.

```sh
DEVBOX_DASHBOARD_DATA_MODE=local-dev-fixtures
```

Uses typed local-dev fixtures after WorkOS authentication. This is the default
outside production so the product shell can run before hosted API credentials
exist.
91 changes: 91 additions & 0 deletions devbox-web/apps/web/src/components/app-shell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Link, useRouterState } from "@tanstack/react-router"
import { FolderOpen, Laptop, LogOut, PanelLeft } from "lucide-react"
import type { ReactNode } from "react"

import { Button } from "@workspace/ui/components/button"

import { authRoutes } from "@/lib/auth"
import type { DashboardData } from "@/lib/dashboard-api"

type AppShellProps = {
data: DashboardData
children: ReactNode
}

const navItems = [
{ to: "/dashboard", label: "Overview", icon: PanelLeft, id: "overview" },
{
to: "/dashboard/folders",
label: "Folders",
icon: FolderOpen,
id: "folders",
},
{
to: "/dashboard/machines",
label: "Machines",
icon: Laptop,
id: "machines",
},
] as const

export function AppShell({ data, children }: AppShellProps) {
const pathname = useRouterState({
select: (state) => state.location.pathname,
})
const active = activeNavItem(pathname)

return (
<div className="min-h-svh bg-background">
<header className="border-b bg-background">
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-5 py-4">
<div className="min-w-0">
<p className="text-sm text-muted-foreground">Devbox</p>
<h1 className="truncate text-xl font-semibold tracking-normal">
{data.identity.account.displayName}
</h1>
</div>
<Button asChild variant="outline" size="sm">
<a href={authRoutes.signOut}>
<LogOut />
Sign out
</a>
</Button>
</div>
</header>
<div className="mx-auto grid max-w-6xl gap-6 px-5 py-6 md:grid-cols-[180px_1fr]">
<nav className="flex gap-2 md:flex-col">
{navItems.map((item) => {
const Icon = item.icon

return (
<Button
key={item.id}
asChild
variant={active === item.id ? "secondary" : "ghost"}
className="justify-start"
>
<Link to={item.to}>
<Icon />
{item.label}
</Link>
</Button>
)
})}
</nav>
<main className="min-w-0 space-y-6">{children}</main>
</div>
</div>
)
}

function activeNavItem(pathname: string): (typeof navItems)[number]["id"] {
if (pathname.startsWith("/dashboard/folders")) {
return "folders"
}

if (pathname.startsWith("/dashboard/machines")) {
return "machines"
}

return "overview"
}
83 changes: 83 additions & 0 deletions devbox-web/apps/web/src/components/dashboard-sections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Badge } from "@workspace/ui/components/badge"

import type {
DashboardData,
MachineSummary,
SharedFolderSummary,
} from "@/lib/dashboard-api"

export function OverviewSection({ data }: { data: DashboardData }) {
return (
<div className="grid gap-3 sm:grid-cols-3">
<Metric label="Folders" value={data.overview.folderCount} />
<Metric label="Machines" value={data.overview.machineCount} />
<Metric label="Trusted" value={data.overview.trustedMachineCount} />
</div>
)
}

export function FolderList({ folders }: { folders: SharedFolderSummary[] }) {
return (
<div className="overflow-hidden rounded-lg border">
{folders.map((folder) => (
<div
key={folder.id}
className="grid gap-3 border-b p-4 last:border-b-0 sm:grid-cols-[1fr_auto]"
>
<div className="min-w-0">
<h2 className="truncate font-medium">{folder.displayName}</h2>
<p className="mt-1 text-sm text-muted-foreground">
{folder.lastCheckpoint}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary">{folder.role}</Badge>
<Badge variant="outline">{folder.hydrationState}</Badge>
</div>
</div>
))}
</div>
)
}

export function MachineList({ machines }: { machines: MachineSummary[] }) {
return (
<div className="overflow-hidden rounded-lg border">
{machines.map((machine) => (
<div
key={machine.id}
className="grid gap-3 border-b p-4 last:border-b-0 sm:grid-cols-[1fr_auto]"
>
<div className="min-w-0">
<h2 className="truncate font-medium">{machine.displayName}</h2>
<p className="mt-1 text-sm text-muted-foreground">
{machine.lastSeen}
</p>
</div>
<Badge
variant={machine.trustState === "trusted" ? "secondary" : "outline"}
>
{machine.trustState}
</Badge>
</div>
))}
</div>
)
}

export function DataSourceNote({ data }: { data: DashboardData }) {
return (
<p className="text-sm text-muted-foreground">
Data source: {data.source.label}
</p>
)
}

function Metric({ label, value }: { label: string; value: number }) {
return (
<div className="rounded-lg border p-4">
<p className="text-sm text-muted-foreground">{label}</p>
<p className="mt-2 text-2xl font-semibold tracking-normal">{value}</p>
</div>
)
}
10 changes: 10 additions & 0 deletions devbox-web/apps/web/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ export const authRoutes = {
signOut: "/auth/sign-out",
} as const

export const defaultSignedInPath = "/dashboard"

export function safeReturnPathname(value: string | null): string | undefined {
if (!value || !value.startsWith("/") || value.startsWith("//")) {
return undefined
}

return value
}

export type WorkOsAuthEnv = {
clientId?: string
apiKey?: string
Expand Down
Loading
Loading