refactor(web): apply dashboard-style worker shell with progressive disclosure

This commit is contained in:
Benjamin Shafii
2026-02-22 15:10:18 -08:00
parent a23dcec9d9
commit c7fd67e47c
2 changed files with 335 additions and 114 deletions

View File

@@ -327,17 +327,32 @@ body {
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.ow-nav-group {
display: grid;
gap: 0.34rem;
}
.ow-nav-label {
margin: 0;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #64748b;
padding-left: 0.22rem;
}
.ow-nav-item {
border: 1px solid #dbe3f4;
border-radius: 0.72rem;
background: #fff;
color: #334155;
font-size: 0.76rem;
font-size: 0.8rem;
font-weight: 700;
padding: 0.54rem 0.48rem;
padding: 0.58rem 0.5rem;
text-align: left;
}
@@ -350,7 +365,7 @@ body {
.ow-app-nav-footer {
margin-top: auto;
border-top: 1px solid #dbe3f4;
padding-top: 0.46rem;
padding-top: 0.58rem;
display: grid;
gap: 0.34rem;
}
@@ -374,6 +389,104 @@ body {
gap: 0.16rem;
}
.ow-pane-head-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.6rem;
}
.ow-pane-block {
border: 1px solid #e3e9f7;
border-radius: 0.86rem;
background: #fbfcff;
padding: 0.62rem;
display: grid;
gap: 0.55rem;
}
.ow-btn-compact {
font-size: 0.78rem;
padding: 0.54rem 0.72rem;
}
.ow-filter-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.5rem;
}
.ow-select {
border-radius: 0.78rem;
border: 1px solid #d9e1f2;
background: #fff;
color: #334155;
padding: 0.76rem 0.8rem;
font-size: 0.86rem;
font-weight: 600;
}
.ow-select:focus-visible {
outline: none;
border-color: rgba(27, 41, 255, 0.55);
box-shadow: 0 0 0 0.22rem rgba(27, 41, 255, 0.16);
}
.ow-overview-grid {
display: grid;
gap: 0.5rem;
}
@media (min-width: 700px) {
.ow-overview-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.ow-overview-card {
border: 1px solid #e3e9f7;
border-radius: 0.86rem;
background: #fbfcff;
padding: 0.64rem;
display: grid;
gap: 0.22rem;
}
.ow-overview-label {
margin: 0;
font-size: 0.7rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.ow-overview-value {
margin: 0;
font-size: 0.94rem;
font-weight: 700;
color: #1e293b;
}
.ow-overview-value.is-ready {
color: #0f9f66;
}
.ow-overview-value.is-starting {
color: #1e3a8a;
}
.ow-overview-value.is-attention {
color: #b45309;
}
.ow-howto-copy {
margin: 0;
color: #64748b;
font-size: 0.75rem;
line-height: 1.4;
}
.ow-empty-detail {
border: 1px dashed #d1ddef;
border-radius: 0.82rem;
@@ -402,6 +515,14 @@ body {
max-width: 11rem;
text-align: right;
}
.ow-filter-row {
grid-template-columns: 1fr;
}
.ow-select {
width: 100%;
}
}
.ow-caption {

View File

@@ -5,6 +5,7 @@ import { FormEvent, useEffect, useState } from "react";
type Step = 1 | 2;
type AuthMode = "sign-in" | "sign-up";
type ShellView = "workers" | "billing";
type WorkerStatusBucket = "ready" | "starting" | "attention" | "other";
type AuthUser = {
id: string;
@@ -243,11 +244,32 @@ function getWorkersList(payload: unknown): WorkerListItem[] {
return rows;
}
function getWorkerStatusMeta(status: string): { label: string; bucket: WorkerStatusBucket } {
const normalized = status.trim().toLowerCase();
if (normalized === "healthy" || normalized === "ready") {
return { label: "Ready", bucket: "ready" };
}
if (normalized === "provisioning" || normalized === "starting") {
return { label: "Starting", bucket: "starting" };
}
if (normalized === "failed" || normalized === "suspended") {
return { label: "Needs attention", bucket: "attention" };
}
return { label: "Unknown", bucket: "other" };
}
function getWorkerStatusCopy(status: string): string {
switch (status) {
const normalized = status.trim().toLowerCase();
switch (normalized) {
case "provisioning":
case "starting":
return "Starting... This may take a minute.";
case "healthy":
case "ready":
return "Ready to connect.";
case "failed":
return "Worker failed to start.";
@@ -550,6 +572,9 @@ export function CloudControlPanel() {
const [events, setEvents] = useState<LaunchEvent[]>([]);
const [copiedField, setCopiedField] = useState<string | null>(null);
const [tokenFetchedForWorkerId, setTokenFetchedForWorkerId] = useState<string | null>(null);
const [workerQuery, setWorkerQuery] = useState("");
const [workerStatusFilter, setWorkerStatusFilter] = useState<WorkerStatusBucket | "all">("all");
const [showLaunchForm, setShowLaunchForm] = useState(false);
const selectedWorker = workers.find((item) => item.workerId === workerLookupId) ?? null;
const activeWorker: WorkerLaunch | null =
@@ -570,6 +595,27 @@ export function CloudControlPanel() {
activeWorker?.workerName ?? null,
);
const filteredWorkers = workers.filter((item) => {
const query = workerQuery.trim().toLowerCase();
const matchesQuery =
!query ||
item.workerName.toLowerCase().includes(query) ||
item.workerId.toLowerCase().includes(query);
if (!matchesQuery) {
return false;
}
if (workerStatusFilter === "all") {
return true;
}
return getWorkerStatusMeta(item.status).bucket === workerStatusFilter;
});
const selectedWorkerStatus = activeWorker?.status ?? selectedWorker?.status ?? "unknown";
const selectedStatusMeta = getWorkerStatusMeta(selectedWorkerStatus);
function appendEvent(level: EventLevel, label: string, detail: string) {
setEvents((current) => {
const next: LaunchEvent[] = [
@@ -819,6 +865,16 @@ export function CloudControlPanel() {
setStep(1);
}, [worker, user, checkoutUrl, paymentReturned]);
useEffect(() => {
if (step !== 2) {
return;
}
if (workers.length === 0) {
setShowLaunchForm(true);
}
}, [step, workers.length]);
useEffect(() => {
if (!user || !worker) {
return;
@@ -957,6 +1013,9 @@ export function CloudControlPanel() {
setLaunchBusy(false);
setStep(1);
setShellView("workers");
setWorkerQuery("");
setWorkerStatusFilter("all");
setShowLaunchForm(false);
setAuthMode("sign-in");
setPassword("");
setAuthInfo("Sign in to launch and manage cloud workers.");
@@ -1024,6 +1083,7 @@ export function CloudControlPanel() {
setWorkerLookupId(parsedWorker.workerId);
setPaymentReturned(false);
setCheckoutUrl(null);
setShowLaunchForm(false);
if (resolvedWorker.status === "provisioning") {
setLaunchStatus("Provisioning started. This can take a few minutes, and we will keep checking automatically.");
@@ -1317,22 +1377,26 @@ export function CloudControlPanel() {
{step === 2 ? (
<div className="ow-app-shell">
<aside className="ow-app-nav">
<button
type="button"
className={`ow-nav-item ${shellView === "workers" ? "is-active" : ""}`}
onClick={() => setShellView("workers")}
>
workers
</button>
<button
type="button"
className={`ow-nav-item ${shellView === "billing" ? "is-active" : ""}`}
onClick={() => setShellView("billing")}
>
billing
</button>
<div className="ow-nav-group">
<p className="ow-nav-label">Workspace</p>
<button
type="button"
className={`ow-nav-item ${shellView === "workers" ? "is-active" : ""}`}
onClick={() => setShellView("workers")}
>
Workers
</button>
<button
type="button"
className={`ow-nav-item ${shellView === "billing" ? "is-active" : ""}`}
onClick={() => setShellView("billing")}
>
Billing
</button>
</div>
<div className="ow-app-nav-footer">
<p className="ow-nav-label">Account</p>
<p className="ow-caption ow-nav-email">{(user?.email ?? email) || "account"}</p>
<button type="button" className="ow-link" onClick={() => void handleSignOut()} disabled={authBusy}>
{authBusy ? "Signing out..." : "Log out"}
@@ -1343,107 +1407,145 @@ export function CloudControlPanel() {
{shellView === "workers" ? (
<>
<section className="ow-pane ow-workers-pane">
<div className="ow-pane-head">
<p className="ow-section-title">workers</p>
<p className="ow-caption">Launch and browse cloud workers.</p>
<div className="ow-pane-head ow-pane-head-row">
<div>
<p className="ow-section-title">Workers</p>
<p className="ow-caption">Pick a worker to see details.</p>
</div>
<button type="button" className="ow-btn-secondary ow-btn-compact" onClick={() => setShowLaunchForm((current) => !current)}>
{showLaunchForm ? "Hide launch" : "Launch worker"}
</button>
</div>
<label className="ow-field-block">
<span className="ow-field-label">Worker Name</span>
<input
className="ow-input"
value={workerName}
onChange={(event) => setWorkerName(event.target.value)}
maxLength={80}
/>
</label>
{showLaunchForm ? (
<div className="ow-pane-block">
<label className="ow-field-block">
<span className="ow-field-label">Worker Name</span>
<input
className="ow-input"
value={workerName}
onChange={(event) => setWorkerName(event.target.value)}
maxLength={80}
/>
</label>
<button
type="button"
className="ow-btn-primary"
onClick={handleLaunchWorker}
disabled={!user || launchBusy || worker?.status === "provisioning"}
>
{launchBusy
? "Requesting launch..."
: worker?.status === "provisioning"
? "Provisioning in progress..."
: `Launch "${workerName || "Cloud Worker"}"`}
</button>
<button
type="button"
className="ow-btn-primary"
onClick={handleLaunchWorker}
disabled={!user || launchBusy || worker?.status === "provisioning"}
>
{launchBusy
? "Starting worker..."
: worker?.status === "provisioning"
? "Worker is starting..."
: `Launch "${workerName || "Cloud Worker"}"`}
</button>
<div className="ow-note-box">
<p>{launchStatus}</p>
{launchError ? <p className="ow-error-text">{launchError}</p> : null}
</div>
{(launchStatus || launchError) && showLaunchForm ? (
<div className="ow-note-box">
<p>{launchStatus}</p>
{launchError ? <p className="ow-error-text">{launchError}</p> : null}
</div>
) : null}
{checkoutUrl ? (
<div className="ow-paywall-box">
<p className="ow-paywall-title">Payment required</p>
<a href={checkoutUrl} rel="noreferrer" className="ow-btn-secondary ow-full">
Continue to Polar checkout
</a>
<p className="ow-caption">After checkout, return and click launch again.</p>
{checkoutUrl ? (
<div className="ow-paywall-box">
<p className="ow-paywall-title">Payment needed before launch</p>
<a href={checkoutUrl} rel="noreferrer" className="ow-btn-secondary ow-full">
Continue to checkout
</a>
<p className="ow-caption">After payment, come back and click launch again.</p>
</div>
) : null}
</div>
) : null}
<div className="ow-pane-head">
<p className="ow-section-title">Worker list</p>
<p className="ow-caption">Select a worker to view details.</p>
<div className="ow-filter-row">
<input
className="ow-input"
value={workerQuery}
onChange={(event) => setWorkerQuery(event.target.value)}
placeholder="Search workers"
aria-label="Search workers"
/>
<select
className="ow-select"
value={workerStatusFilter}
onChange={(event) => setWorkerStatusFilter(event.target.value as WorkerStatusBucket | "all")}
>
<option value="all">All statuses</option>
<option value="ready">Ready</option>
<option value="starting">Starting</option>
<option value="attention">Needs attention</option>
</select>
</div>
{workersBusy ? <p className="ow-caption">Loading workers...</p> : null}
{workersError ? <p className="ow-error-text">{workersError}</p> : null}
{workers.length > 0 ? (
{filteredWorkers.length > 0 ? (
<ul className="ow-worker-list ow-worker-list-panel">
{workers.map((item) => (
<li key={item.workerId}>
<button
type="button"
className={`ow-worker-select ${workerLookupId === item.workerId ? "is-active" : ""}`}
onClick={() => {
setWorkerLookupId(item.workerId);
setWorker((current) => listItemToWorker(item, current));
}}
>
<div className="ow-worker-head">
<div>
<p className="ow-step-title">{item.workerName}</p>
<p className="ow-step-detail">
{item.status === "healthy" ? "Ready" : item.status === "provisioning" ? "Starting..." : item.status}
</p>
{filteredWorkers.map((item) => {
const meta = getWorkerStatusMeta(item.status);
return (
<li key={item.workerId}>
<button
type="button"
className={`ow-worker-select ${workerLookupId === item.workerId ? "is-active" : ""}`}
onClick={() => {
setWorkerLookupId(item.workerId);
setWorker((current) => listItemToWorker(item, current));
}}
>
<div className="ow-worker-head">
<div>
<p className="ow-step-title">{item.workerName}</p>
<p className="ow-step-detail">{meta.label}</p>
</div>
{item.isMine ? <span className="ow-badge">Yours</span> : null}
</div>
{item.isMine ? <span className="ow-badge">Yours</span> : null}
</div>
</button>
</li>
))}
</button>
</li>
);
})}
</ul>
) : null}
{workers.length > 0 && filteredWorkers.length === 0 ? (
<p className="ow-caption">No workers match this filter.</p>
) : null}
{workers.length === 0 && !workersBusy ? (
<p className="ow-caption">No workers yet. Launch one and it will appear here automatically.</p>
<p className="ow-caption">No workers yet. Launch one to get started.</p>
) : null}
</section>
<section className="ow-pane ow-detail-pane">
{selectedWorker ? (
<>
<div className="ow-worker-head">
<div>
<p className="ow-section-title">{activeWorker?.workerName ?? selectedWorker.workerName}</p>
<p className="ow-step-detail">
{(activeWorker?.status ?? selectedWorker.status) === "healthy"
? "Ready"
: (activeWorker?.status ?? selectedWorker.status) === "provisioning"
? "Starting..."
: (activeWorker?.status ?? selectedWorker.status)}
</p>
<div className="ow-pane-head">
<p className="ow-section-title">Overview</p>
<div className="ow-worker-head">
<div>
<p className="ow-step-title">{activeWorker?.workerName ?? selectedWorker.workerName}</p>
<p className="ow-step-detail">{selectedStatusMeta.label}</p>
</div>
{selectedWorker.isMine ? <span className="ow-badge">Yours</span> : null}
</div>
{selectedWorker.isMine ? <span className="ow-badge">Yours</span> : null}
<p className="ow-caption">{getWorkerStatusCopy(selectedWorkerStatus)}</p>
</div>
<p className="ow-caption">{getWorkerStatusCopy(activeWorker?.status ?? selectedWorker.status)}</p>
<div className="ow-overview-grid">
<div className="ow-overview-card">
<p className="ow-overview-label">Status</p>
<p className={`ow-overview-value is-${selectedStatusMeta.bucket}`}>{selectedStatusMeta.label}</p>
</div>
<div className="ow-overview-card">
<p className="ow-overview-label">Connect</p>
<p className="ow-overview-value">{openworkDeepLink ? "One-click ready" : "Preparing"}</p>
</div>
</div>
<div className="ow-inline-actions">
<button
@@ -1455,7 +1557,7 @@ export function CloudControlPanel() {
}
window.location.href = openworkDeepLink;
}}
disabled={!openworkDeepLink || (activeWorker?.status ?? selectedWorker.status) !== "healthy"}
disabled={!openworkDeepLink || selectedStatusMeta.bucket !== "ready"}
>
{openworkDeepLink ? "Open in OpenWork" : "Preparing connection..."}
</button>
@@ -1463,23 +1565,21 @@ export function CloudControlPanel() {
{!openworkDeepLink || !openworkConnectUrl || (!hasWorkspaceScopedUrl && openworkConnectUrl) ? (
<div className="ow-note-box">
{!openworkDeepLink ? <p className="ow-caption">Waiting for worker URL and token.</p> : null}
{!openworkConnectUrl ? <p className="ow-caption">Keep this tab open.</p> : null}
{!hasWorkspaceScopedUrl && openworkConnectUrl ? (
<p className="ow-caption">Connection URL is still preparing...</p>
) : null}
{!openworkDeepLink ? <p className="ow-caption">Getting connection details ready...</p> : null}
{!openworkConnectUrl ? <p className="ow-caption">Keep this page open for a moment.</p> : null}
{!hasWorkspaceScopedUrl && openworkConnectUrl ? <p className="ow-caption">Finishing your workspace URL...</p> : null}
</div>
) : null}
<details className="ow-howto">
<summary>Manual connect details</summary>
<p className="ow-caption" style={{ marginBottom: "0.5rem" }}>
If the "Open in OpenWork" button doesn't work, copy these details into the OpenWork app manually (Add a worker &gt; Connect remote).
<summary>Copy details manually</summary>
<p className="ow-howto-copy">
If one-click open doesn't work, copy these into OpenWork: Add a worker &gt; Connect remote.
</p>
<CredentialRow
label="OpenWork worker URL"
value={openworkConnectUrl}
placeholder="URL becomes available after provisioning."
placeholder="URL appears once ready"
canCopy={Boolean(openworkConnectUrl)}
copied={copiedField === "openwork-url"}
onCopy={() => void copyToClipboard("openwork-url", openworkConnectUrl)}
@@ -1488,7 +1588,7 @@ export function CloudControlPanel() {
<CredentialRow
label="Access token"
value={activeWorker?.clientToken ?? null}
placeholder="Use manual refresh to get access token."
placeholder="Use more options to refresh"
canCopy={Boolean(activeWorker?.clientToken)}
copied={copiedField === "access-token"}
onCopy={() => void copyToClipboard("access-token", activeWorker?.clientToken ?? null)}
@@ -1500,7 +1600,7 @@ export function CloudControlPanel() {
</details>
<details className="ow-howto">
<summary>Manual refresh and diagnostics</summary>
<summary>More options</summary>
<div className="ow-inline-actions">
<button
type="button"
@@ -1524,13 +1624,13 @@ export function CloudControlPanel() {
onClick={handleGenerateKey}
disabled={actionBusy !== null}
>
{actionBusy === "token" ? "Fetching..." : "Get access token"}
{actionBusy === "token" ? "Fetching..." : "Refresh token"}
</button>
</div>
</details>
<details className="ow-howto">
<summary>Advanced</summary>
<summary>Technical details</summary>
<CredentialRow
label="Worker host URL"
value={activeWorker?.instanceUrl ?? null}
@@ -1551,7 +1651,7 @@ export function CloudControlPanel() {
{events.length > 0 ? (
<div className="ow-log-box">
<p className="ow-section-title">Launch log</p>
<p className="ow-section-title">Recent activity</p>
<ul className="ow-log-list">
{events.map((entry) => (
<li key={entry.id} className={`ow-log-item level-${entry.level}`}>
@@ -1569,8 +1669,8 @@ export function CloudControlPanel() {
</>
) : (
<div className="ow-empty-detail">
<p className="ow-section-title">Launch your first worker</p>
<p className="ow-caption">Use the launch form in the list panel, then select the worker here.</p>
<p className="ow-section-title">Select a worker</p>
<p className="ow-caption">Pick a worker from the list to see details and connect.</p>
</div>
)}
</section>
@@ -1578,21 +1678,21 @@ export function CloudControlPanel() {
) : (
<section className="ow-pane ow-billing-pane ow-pane-span-2">
<div className="ow-pane-head">
<p className="ow-section-title">billing</p>
<p className="ow-caption">Manage plan and checkout details.</p>
<p className="ow-section-title">Billing</p>
<p className="ow-caption">Handle checkout when launching a new worker.</p>
</div>
{checkoutUrl ? (
<div className="ow-paywall-box">
<p className="ow-paywall-title">Pending checkout</p>
<p className="ow-paywall-title">Checkout in progress</p>
<a href={checkoutUrl} rel="noreferrer" className="ow-btn-secondary ow-full">
Continue to Polar checkout
Continue to checkout
</a>
</div>
) : (
<div className="ow-note-box">
<p>Billing appears automatically during worker launch if payment is required.</p>
<p className="ow-caption">Switch back to workers and click launch to continue onboarding.</p>
<p>No payment action right now.</p>
<p className="ow-caption">Go to Workers and launch a worker when you're ready.</p>
</div>
)}
</section>