Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4861cca
feat(dashboard): add internal workflow run page with DAG graph
stylessh Apr 22, 2026
0e615a9
perf(dashboard): reduce workflow graph re-renders
stylessh Apr 22, 2026
3e08310
perf(dashboard): isolate summary ticker to leaf components
stylessh Apr 22, 2026
3358c7d
feat(dashboard): step log viewer with live polling, groups, and drag
stylessh Apr 23, 2026
2426c3d
feat(dashboard): add workflow job detail page with shared run chrome
stylessh Apr 23, 2026
f47629c
refactor(dashboard): use TanStack Link for internal job nav and let j…
stylessh Apr 23, 2026
2aee65f
fix(dashboard): address PR review feedback on workflow run pages
stylessh Apr 23, 2026
a3d2375
feat(dashboard): match GitHub /job/ URL and hash-linked step rows
stylessh Apr 23, 2026
766f9c9
feat(dashboard): deep-link graph step-log card to job step anchor
stylessh Apr 23, 2026
ab30fc7
feat(dashboard): slugify step anchor hash
stylessh Apr 23, 2026
d40642c
feat(dashboard): collapse log groups by default
stylessh Apr 23, 2026
2b36167
fix(dashboard): match step log group by timestamp, not just name
stylessh Apr 23, 2026
a80d617
revert(dashboard): simpler step log extractor, split log once per poll
stylessh Apr 23, 2026
06165fc
style(dashboard): align sidebar job link hover/active with tabs
stylessh Apr 23, 2026
4b1031b
feat(dashboard): repo actions page + accurate per-step logs via run zip
stylessh Apr 25, 2026
392db5f
feat(dashboard): cached workflow run/job pages with webhook revalidation
stylessh Apr 25, 2026
5b49568
feat(dashboard): persist workflow run + job tabs with status colors
stylessh Apr 25, 2026
fe5e21f
fix(dashboard): tab + StatePill polish on PR / workflow-run pages
stylessh Apr 25, 2026
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
4 changes: 3 additions & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@tanstack/react-start": "~1.167.23",
"@tanstack/react-virtual": "^3.13.24",
"@tanstack/router-plugin": "~1.167.12",
"@xyflow/react": "^12.10.2",
"agentation": "^3.0.2",
"better-auth": "^1.6.0",
"drizzle-orm": "^0.45.2",
Expand All @@ -47,7 +48,8 @@
"react-dom": "^19.2.0",
"react-dropzone": "^15.0.0",
"recharts": "^3.8.1",
"tailwindcss": "^4.1.18"
"tailwindcss": "^4.1.18",
"yaml": "^2.8.3"
},
"devDependencies": {
"@biomejs/biome": "2.4.5",
Expand Down
112 changes: 112 additions & 0 deletions apps/dashboard/src/components/checks/check-state-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { CheckIcon, XIcon } from "@diffkit/icons";

export type CheckState =
| "success"
| "failure"
| "pending"
| "waiting"
| "skipped"
| "expected";

export function getCheckState(input: {
status: string;
conclusion: string | null;
}): CheckState {
if (input.status === "expected") return "expected";
if (
input.status === "queued" ||
input.status === "waiting" ||
input.status === "pending"
) {
return "waiting";
}
if (input.status !== "completed" || input.conclusion === null) {
return "pending";
}
if (input.conclusion === "success" || input.conclusion === "neutral") {
return "success";
}
if (input.conclusion === "skipped" || input.conclusion === "stale") {
return "skipped";
}
return "failure";
}

export function CheckStateIcon({ state }: { state: CheckState }) {
if (state === "success") {
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-green-600 dark:text-green-400">
<CheckIcon size={12} strokeWidth={3} />
</div>
);
}
if (state === "failure") {
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-red-600 dark:text-red-400">
<XIcon size={12} strokeWidth={3} />
</div>
);
}
if (state === "skipped") {
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-muted-foreground">
<div className="size-1.5 rounded-full border border-current" />
</div>
);
}
if (state === "waiting") {
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-muted-foreground">
<svg
className="size-3.5"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<circle
cx="8"
cy="8"
r="6"
stroke="currentColor"
strokeWidth="2"
opacity="0.35"
/>
</svg>
</div>
);
}
if (state === "expected") {
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-yellow-500">
<div className="size-1.5 rounded-full bg-current" />
</div>
);
}
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-yellow-500">
<div className="size-3.5 animate-spin">
<svg
className="size-3.5"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<circle
cx="8"
cy="8"
r="6"
stroke="currentColor"
strokeWidth="2"
opacity="0.25"
/>
<path
d="M14 8a6 6 0 0 0-6-6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</div>
</div>
);
}
106 changes: 27 additions & 79 deletions apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { quickhubDark, quickhubLight } from "@diffkit/ui/lib/diffs-themes";
import { cn } from "@diffkit/ui/lib/utils";
import type { DiffLineAnnotation, PatchDiffProps } from "@pierre/diffs/react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { useTheme } from "next-themes";
import {
type ComponentType,
Expand All @@ -55,6 +56,10 @@ import {
useRef,
useState,
} from "react";
import {
CheckStateIcon,
getCheckState,
} from "#/components/checks/check-state-icon";
import { CommentMoreMenu } from "#/components/details/comment-more-menu";
import { IssueCommentReactionBar } from "#/components/details/comment-reaction-bar";
import { CommentReplyForm } from "#/components/details/comment-reply-form";
Expand Down Expand Up @@ -637,8 +642,8 @@ function ReviewsSection({
key={review.id}
className="flex items-center gap-2 px-4 py-1.5 pl-11"
>
<CheckRunIcon
status={review.state === "APPROVED" ? "success" : "failure"}
<CheckStateIcon
state={review.state === "APPROVED" ? "success" : "failure"}
/>
{review.author && (
<img
Expand Down Expand Up @@ -808,17 +813,18 @@ function ChecksSection({
// Group by status in display order: expected, failed, pending, skipped, passed
const groupedRuns = useMemo(() => {
const groups: Record<
"expected" | "failure" | "pending" | "skipped" | "success",
"expected" | "failure" | "pending" | "waiting" | "skipped" | "success",
PullCheckRun[]
> = {
expected: [],
failure: [],
pending: [],
waiting: [],
skipped: [],
success: [],
};
for (const run of checkRuns) {
groups[getCheckRunStatus(run)].push(run);
groups[getCheckState(run)].push(run);
}
return groups;
}, [checkRuns]);
Expand All @@ -830,6 +836,7 @@ function ChecksSection({
{ key: "expected", label: "Expected" },
{ key: "failure", label: "Failed" },
{ key: "pending", label: "Pending" },
{ key: "waiting", label: "Waiting" },
{ key: "skipped", label: "Skipped" },
{ key: "success", label: "Passed" },
];
Expand Down Expand Up @@ -975,7 +982,7 @@ function ChecksSection({
{label}
</div>
{runs.map((run) => {
const runStatus = getCheckRunStatus(run);
const runStatus = getCheckState(run);
const detail =
runStatus === "expected"
? "Waiting for status to be reported"
Expand All @@ -998,15 +1005,28 @@ function ChecksSection({
key={`${run.name}:${run.id}`}
className="group/run flex items-center gap-2 px-4 py-1.5 pl-11"
>
<CheckRunIcon status={runStatus} />
<CheckStateIcon state={runStatus} />
{run.appAvatarUrl && (
<img
src={run.appAvatarUrl}
alt=""
className="size-4 shrink-0 rounded border border-border"
/>
)}
{run.htmlUrl ? (
{run.workflowRunId != null ? (
<Link
to="/$owner/$repo/actions/runs/$runId"
params={{
owner,
repo,
runId: String(run.workflowRunId),
}}
search={{ pr: pullNumber }}
className="min-w-0 flex-1 truncate text-xs hover:underline"
>
{nameContent}
</Link>
) : run.htmlUrl ? (
<a
href={run.htmlUrl}
target="_blank"
Expand Down Expand Up @@ -1535,78 +1555,6 @@ function StatusIcon({ status }: { status: StatusType }) {
);
}

function CheckRunIcon({
status,
}: {
status: "success" | "failure" | "pending" | "skipped" | "expected";
}) {
if (status === "success") {
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-green-600 dark:text-green-400">
<CheckIcon size={12} strokeWidth={3} />
</div>
);
}
if (status === "failure") {
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-red-600 dark:text-red-400">
<XIcon size={12} strokeWidth={3} />
</div>
);
}
if (status === "skipped") {
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-muted-foreground">
<div className="size-1.5 rounded-full border border-current" />
</div>
);
}
if (status === "expected") {
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-yellow-500">
<div className="size-1.5 rounded-full bg-current" />
</div>
);
}
return (
<div className="flex size-3.5 shrink-0 items-center justify-center text-yellow-500">
<svg
className="size-3.5 animate-spin"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<circle
cx="8"
cy="8"
r="6"
stroke="currentColor"
strokeWidth="2"
opacity="0.25"
/>
<path
d="M14 8a6 6 0 0 0-6-6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</div>
);
}

function getCheckRunStatus(
run: PullCheckRun,
): "success" | "failure" | "pending" | "skipped" | "expected" {
if (run.status === "expected") return "expected";
if (run.status !== "completed" || run.conclusion === null) return "pending";
if (run.conclusion === "success" || run.conclusion === "neutral")
return "success";
if (run.conclusion === "skipped" || run.conclusion === "stale")
return "skipped";
return "failure";
}

function MergeStatusSkeleton() {
return (
<div className="flex flex-col rounded-lg border">
Expand Down
45 changes: 2 additions & 43 deletions apps/dashboard/src/components/pulls/detail/pull-detail-header.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { FileIcon, GitCommitIcon, ReviewsIcon } from "@diffkit/icons";
import { StatePill } from "@diffkit/ui/components/state-pill";
import {
Callout,
CalloutAction,
CalloutContent,
} from "@diffkit/ui/components/callout";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@diffkit/ui/components/tooltip";
import { StatePill } from "@diffkit/ui/components/state-pill";
import { cn } from "@diffkit/ui/lib/utils";
import { Link } from "@tanstack/react-router";
import { useCallback, useRef, useState } from "react";
import { DetailPageTitle } from "#/components/details/detail-page";
import { CopyBadge } from "#/components/shared/copy-badge";
import type { PullDetail } from "#/lib/github.types";
import { getPrStateConfig } from "#/lib/pr-state";

Expand Down Expand Up @@ -146,42 +141,6 @@ export function PullDetailHeader({

const DIFF_BOX_COUNT = 5;

function CopyBadge({
value,
canTruncate,
}: {
value: string;
canTruncate?: boolean;
}) {
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);

const handleClick = useCallback(() => {
void navigator.clipboard.writeText(value);
setCopied(true);
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setCopied(false), 1500);
}, [value]);

return (
<Tooltip open={copied}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleClick}
className={cn(
"shrink-0 cursor-pointer rounded bg-surface-1 px-1.5 py-0.5 font-mono text-xs font-[550] transition-colors hover:bg-surface-2",
canTruncate && "min-w-0 shrink truncate",
)}
>
{value}
</button>
</TooltipTrigger>
<TooltipContent>Copied!</TooltipContent>
</Tooltip>
);
}

function DiffBoxes({
additions,
deletions,
Expand Down
Loading
Loading