diff --git a/apps/dashboard/src/app/onboarding/_steps/step-1.tsx b/apps/dashboard/src/app/onboarding/_steps/step-1.tsx index 03314132..c9374b0b 100644 --- a/apps/dashboard/src/app/onboarding/_steps/step-1.tsx +++ b/apps/dashboard/src/app/onboarding/_steps/step-1.tsx @@ -43,6 +43,7 @@ export function Step1({ monitorData, checkResults, isStreaming, + checksComplete, onSubmit, onSkip, onContinue, @@ -55,6 +56,7 @@ export function Step1({ monitorData: OnboardingMonitor | undefined; checkResults: ReturnType["results"]; isStreaming: boolean; + checksComplete: boolean; onSubmit: (values: { url: string }) => Promise; onSkip: () => void; onContinue: () => void; @@ -82,6 +84,7 @@ export function Step1({ ); const allFailed = !isStreaming && checkResults.length > 0 && successfulResults.length === 0; + const canContinue = checksComplete; return ( <> @@ -111,8 +114,14 @@ export function Step1({ )} {isLocked ? ( - ) : ( <> diff --git a/apps/dashboard/src/app/onboarding/_steps/step-2.tsx b/apps/dashboard/src/app/onboarding/_steps/step-2.tsx index 3187fdd3..05863f60 100644 --- a/apps/dashboard/src/app/onboarding/_steps/step-2.tsx +++ b/apps/dashboard/src/app/onboarding/_steps/step-2.tsx @@ -1,9 +1,9 @@ "use client"; -import type { ThemeKey } from "@openstatus/theme-store"; +import type { CheckResult } from "@openstatus/services/monitor"; import { Button } from "@openstatus/ui/components/ui/button"; import { ArrowRight } from "lucide-react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { CreatePageForm, @@ -22,6 +22,7 @@ import { OnboardingStepper, } from "@/components/layout/onboarding-layout"; import { OnboardingStatusPagePreview } from "@/components/onboarding/status-page-preview"; +import { deriveTodayBar } from "@/lib/onboarding/live-status"; import type { OnboardingPage } from "../client"; @@ -32,6 +33,7 @@ export function Step2({ slugFallback, monitorSkipped, monitorName, + checkResults, isSubmitting, onSubmit, onSkip, @@ -43,6 +45,7 @@ export function Step2({ slugFallback: string; monitorSkipped: boolean; monitorName: string | null; + checkResults: CheckResult[]; isSubmitting: boolean; onSubmit: (values: CreatePageFormValues) => Promise; onSkip: () => void; @@ -50,6 +53,11 @@ export function Step2({ }) { const isLocked = pageStatus === "completed" && !!createdPageData; + const monitorTodayBar = useMemo( + () => (isLocked || monitorSkipped ? null : deriveTodayBar(checkResults)), + [isLocked, monitorSkipped, checkResults], + ); + // Live preview tracks the form values via `onValuesChange` below. Defaults // mirror the form's initial state so the preview renders something on first // paint before the user touches the inputs. @@ -130,7 +138,8 @@ export function Step2({ ).replace(/-/g, " ")} components={lockedPreviewValues.components ?? []} monitorName={monitorName} - themeKey={lockedPreviewValues.theme as ThemeKey} + monitorTodayBar={monitorTodayBar} + themeKey={lockedPreviewValues.theme} className="h-full w-full" /> diff --git a/apps/dashboard/src/app/onboarding/client.tsx b/apps/dashboard/src/app/onboarding/client.tsx index cd8687bc..c11f6b23 100644 --- a/apps/dashboard/src/app/onboarding/client.tsx +++ b/apps/dashboard/src/app/onboarding/client.tsx @@ -124,6 +124,7 @@ export function Client() { const { results: checkResults, isStreaming, + isComplete: checksComplete, start: startChecks, stop: stopChecks, } = useStreamChecks(); @@ -204,6 +205,7 @@ export function Client() { monitorData={monitorData} checkResults={checkResults} isStreaming={isStreaming} + checksComplete={checksComplete} onSubmit={async (values) => { await createMonitorMutation.mutateAsync({ url: values.url, @@ -235,6 +237,7 @@ export function Client() { monitorName={ monitorData?.url ? safeHostname(monitorData.url) : null } + checkResults={checkResults} isSubmitting={createPageMutation.isPending} onSubmit={async (values) => { if (!workspace?.id) return; diff --git a/apps/dashboard/src/components/onboarding/status-page-preview.tsx b/apps/dashboard/src/components/onboarding/status-page-preview.tsx index 45888f9d..cf2212aa 100644 --- a/apps/dashboard/src/components/onboarding/status-page-preview.tsx +++ b/apps/dashboard/src/components/onboarding/status-page-preview.tsx @@ -77,12 +77,15 @@ function buildPlaceholderBarData(): StatusBarData[] { }); } +const MONITOR_KEY = "__monitor"; + export type OnboardingStatusPagePreviewProps = { slug: string; title?: string; description?: string; components: { name: string }[]; monitorName?: string | null; + monitorTodayBar?: StatusBarData | null; themeKey: ThemeKey; className?: string; }; @@ -93,6 +96,7 @@ export function OnboardingStatusPagePreview({ description, components, monitorName, + monitorTodayBar, themeKey, className, }: OnboardingStatusPagePreviewProps) { @@ -112,10 +116,17 @@ export function OnboardingStatusPagePreview({ const themeStyle = useThemeVars(themeKey, mode, mounted); const [placeholderBarData] = useState(buildPlaceholderBarData); + const monitorBarData = useMemo( + () => + monitorTodayBar + ? [...placeholderBarData.slice(0, -1), monitorTodayBar] + : placeholderBarData, + [placeholderBarData, monitorTodayBar], + ); const displayTitle = title?.trim() || slug || "My Status Page"; const allComponents: { key: string; name: string }[] = [ - ...(monitorName ? [{ key: "__monitor", name: monitorName }] : []), + ...(monitorName ? [{ key: MONITOR_KEY, name: monitorName }] : []), ...components .filter((c) => c.name.trim().length > 0) .map((c, i) => ({ key: `c-${i}`, name: c.name })), @@ -158,25 +169,29 @@ export function OnboardingStatusPagePreview({ - {allComponents.map((c) => ( - - - - {c.name} - - - 100% - - - - - - - - ))} + {allComponents.map((c) => { + const barData = + c.key === MONITOR_KEY ? monitorBarData : placeholderBarData; + return ( + + + + {c.name} + + + 100% + + + + + + + + ); + })} diff --git a/apps/dashboard/src/components/onboarding/use-stream-checks.ts b/apps/dashboard/src/components/onboarding/use-stream-checks.ts index db3aee02..2bf75b4b 100644 --- a/apps/dashboard/src/components/onboarding/use-stream-checks.ts +++ b/apps/dashboard/src/components/onboarding/use-stream-checks.ts @@ -7,6 +7,7 @@ import { toast } from "sonner"; export function useStreamChecks() { const [results, setResults] = useState([]); const [isStreaming, setIsStreaming] = useState(false); + const [isComplete, setIsComplete] = useState(false); const [error, setError] = useState(null); const abortRef = useRef(null); @@ -29,6 +30,7 @@ export function useStreamChecks() { abortRef.current = controller; setResults([]); setError(null); + setIsComplete(false); setIsStreaming(true); try { @@ -84,6 +86,7 @@ export function useStreamChecks() { fail(err instanceof Error ? err.message : "Stream failed"); } finally { setIsStreaming(false); + setIsComplete(true); abortRef.current = null; } }, @@ -96,5 +99,5 @@ export function useStreamChecks() { }; }, []); - return { results, isStreaming, error, start, stop }; + return { results, isStreaming, isComplete, error, start, stop }; } diff --git a/apps/dashboard/src/lib/onboarding/live-status.ts b/apps/dashboard/src/lib/onboarding/live-status.ts new file mode 100644 index 00000000..868ebcd3 --- /dev/null +++ b/apps/dashboard/src/lib/onboarding/live-status.ts @@ -0,0 +1,25 @@ +import type { CheckResult } from "@openstatus/services/monitor"; +import type { StatusBarData } from "@openstatus/ui/components/blocks/status.types"; + +export function deriveTodayBar(results: CheckResult[]): StatusBarData | null { + if (results.length === 0) return null; + let up = 0; + for (const r of results) { + if (r.state === "success" && r.status >= 200 && r.status <= 399) up++; + } + const count = results.length; + const down = count - up; + const upHeight = Math.round((up / count) * 100); + const downStatus = down / count > 0.5 ? "error" : "degraded"; + const bar: StatusBarData["bar"] = []; + if (up > 0) bar.push({ status: "success", height: upHeight }); + if (down > 0) bar.push({ status: downStatus, height: 100 - upHeight }); + const dayStatus = down === 0 ? "success" : downStatus; + const day = new Date().toISOString().slice(0, 10); + return { + day, + bar, + card: [{ status: dayStatus, value: `${upHeight}%` }], + events: [], + }; +}