Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
"@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",
"fflate": "^0.8.2",
"motion": "^12.38.0",
"next-themes": "^0.4.6",
"nuqs": "^2.8.9",
Expand All @@ -47,7 +49,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
122 changes: 122 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,122 @@
import { CheckIcon, XIcon } from "@diffkit/icons";

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

/** Tailwind text-color class for a CheckState — used by tab icons / inline
* badges that want a single color without the wrapped CheckStateIcon. */
export function getCheckStateColor(state: CheckState): string {
if (state === "success") return "text-green-500";
if (state === "failure") return "text-red-500";
if (state === "pending") return "text-yellow-500";
if (state === "expected") return "text-yellow-500";
return "text-muted-foreground";
}

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>
);
}
8 changes: 8 additions & 0 deletions apps/dashboard/src/components/filters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ export {
repoListUrlParsers,
useRepoListFilters,
} from "./use-repo-list-filters";
export {
deriveApiStatus as deriveWorkflowRunApiStatus,
makeBranchFilterDef,
type RunStatusValue,
repoWorkflowRunFilterDefs,
runStatus,
workflowRunSortOptions,
} from "./workflow-run-filters";
224 changes: 224 additions & 0 deletions apps/dashboard/src/components/filters/workflow-run-filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import {
CalendarIcon,
CheckIcon,
CircleIcon,
ClockIcon,
GitBranchIcon,
LoaderCircleIcon,
MinusSignIcon,
XIcon,
} from "@diffkit/icons";
import { createElement } from "react";
import { authorFilterDef } from "./filter-helpers";
import type {
FilterableItem,
FilterDefinition,
FilterOption,
SortOption,
} from "./use-list-filters";

type WorkflowRunFilterable = FilterableItem & {
status: string;
conclusion: string | null;
event: string;
headBranch: string | null;
};

function asRun(item: FilterableItem): WorkflowRunFilterable {
return item as WorkflowRunFilterable;
}

export type RunStatusValue =
| "queued"
| "in_progress"
| "success"
| "failure"
| "cancelled"
| "skipped";

export function runStatus(item: {
status: string;
conclusion: string | null;
}): RunStatusValue {
if (item.status === "queued" || item.status === "waiting") return "queued";
if (item.status !== "completed") return "in_progress";
if (item.conclusion === "success" || item.conclusion === "neutral") {
return "success";
}
if (item.conclusion === "skipped" || item.conclusion === "stale") {
return "skipped";
}
if (item.conclusion === "cancelled") return "cancelled";
return "failure";
}

const RUN_STATUS_META: readonly {
value: RunStatusValue;
label: string;
icon: React.ComponentType<{ size?: number; className?: string }>;
colorClass: string;
}[] = [
{
value: "in_progress",
label: "In progress",
icon: LoaderCircleIcon,
colorClass: "text-yellow-500",
},
{
value: "queued",
label: "Queued",
icon: ClockIcon,
colorClass: "text-muted-foreground",
},
{
value: "success",
label: "Success",
icon: CheckIcon,
colorClass: "text-green-500",
},
{
value: "failure",
label: "Failure",
icon: XIcon,
colorClass: "text-red-500",
},
{
value: "cancelled",
label: "Cancelled",
icon: MinusSignIcon,
colorClass: "text-muted-foreground",
},
{
value: "skipped",
label: "Skipped",
icon: CircleIcon,
colorClass: "text-muted-foreground",
},
];

function toStatusOption(meta: (typeof RUN_STATUS_META)[number]): FilterOption {
return {
value: meta.value,
label: meta.label,
icon: createElement(meta.icon, { size: 14, className: meta.colorClass }),
};
}

const RUN_STATUS_OPTIONS: readonly FilterOption[] =
RUN_STATUS_META.map(toStatusOption);

const runStatusFilterDef: FilterDefinition = {
id: "status",
label: "Status",
icon: CircleIcon,
extractOptions: () => RUN_STATUS_OPTIONS as FilterOption[],
match: (item, values) => values.has(runStatus(asRun(item))),
};

/** Common GitHub Actions event triggers (static — API does not list them). */
const EVENT_LABELS: Readonly<Record<string, string>> = {
push: "Push",
pull_request: "Pull request",
workflow_dispatch: "Manual",
schedule: "Schedule",
release: "Release",
workflow_run: "Workflow run",
repository_dispatch: "Repository dispatch",
pull_request_target: "Pull request (target)",
check_run: "Check run",
check_suite: "Check suite",
issues: "Issues",
issue_comment: "Issue comment",
deployment: "Deployment",
merge_group: "Merge group",
};

function labelForEvent(event: string): string {
return EVENT_LABELS[event] ?? event;
}

const runEventFilterDef: FilterDefinition = {
id: "event",
label: "Event",
icon: CalendarIcon,
extractOptions: (items) => {
const present = new Set<string>();
for (const item of items) present.add(asRun(item).event);
return [...present].sort().map((event) => ({
value: event,
label: labelForEvent(event),
}));
},
match: (item, values) => values.has(asRun(item).event),
};

/** Build a branch FilterDefinition from a list of branch names (data-driven). */
export function makeBranchFilterDef(
branchNames: readonly string[],
): FilterDefinition {
const options: FilterOption[] = branchNames.map((name) => ({
value: name,
label: name,
}));
return {
id: "branch",
label: "Branch",
icon: GitBranchIcon,
extractOptions: (items) => {
if (options.length > 0) return options;
const present = new Set<string>();
for (const item of items) {
const branch = asRun(item).headBranch;
if (branch) present.add(branch);
}
return [...present].sort().map((name) => ({ value: name, label: name }));
},
match: (item, values) => {
const b = asRun(item).headBranch;
return b ? values.has(b) : false;
},
};
}

export const repoWorkflowRunFilterDefs: FilterDefinition[] = [
runStatusFilterDef,
runEventFilterDef,
authorFilterDef,
];

export const workflowRunSortOptions: SortOption[] = [
{
id: "updated",
label: "Recently updated",
compare: (a, b) =>
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
},
{
id: "created",
label: "Newest first",
compare: (a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
},
{
id: "created-asc",
label: "Oldest first",
compare: (a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
},
];

/** Map a `status` filter pill value to the GitHub API `status` query parameter. */
export function deriveApiStatus(
values: Set<string>,
):
| "queued"
| "in_progress"
| "success"
| "failure"
| "cancelled"
| "skipped"
| undefined {
if (values.size !== 1) return undefined;
const [first] = values;
return first as RunStatusValue;
}
2 changes: 2 additions & 0 deletions apps/dashboard/src/components/layouts/dashboard-tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ActionsIcon,
ArchiveIcon,
ChevronRightIcon,
CloseIcon,
Expand Down Expand Up @@ -35,6 +36,7 @@ const tabIconMap = {
review: ReviewsIcon,
repo: ArchiveIcon,
commit: GitCommitIcon,
actions: ActionsIcon,
} as const;

function useScrollShadows(tabCount: number) {
Expand Down
Loading
Loading