Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
54 changes: 29 additions & 25 deletions src/app/components/dashboard/ActionsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,37 +211,41 @@ export default function ActionsTab(props: ActionsTabProps) {
const highlightedReposActions = createReorderHighlight(
() => repoGroups().map(g => g.repoFullName),
() => viewState.lockedRepos.actions,
() => viewState.ignoredItems.filter(i => i.type === "workflowRun").length,
);

return (
<div class="divide-y divide-base-300">
{/* Toolbar */}
<div class="flex flex-wrap items-center gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
<label class="flex items-center gap-1.5 text-sm text-base-content/70 cursor-pointer select-none">
<input
type="checkbox"
checked={viewState.showPrRuns}
onChange={(e) => setViewState("showPrRuns", e.currentTarget.checked)}
class="checkbox checkbox-sm checkbox-primary"
<div class="flex items-start gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
<div class="flex flex-wrap items-center gap-3 min-w-0 flex-1">
<label class="flex items-center gap-1.5 text-sm text-base-content/70 cursor-pointer select-none">
<input
type="checkbox"
checked={viewState.showPrRuns}
onChange={(e) => setViewState("showPrRuns", e.currentTarget.checked)}
class="checkbox checkbox-sm checkbox-primary"
/>
Show PR runs
</label>
<FilterChips
groups={actionsFilterGroups}
values={viewState.tabFilters.actions}
onChange={(field, value) => setTabFilter("actions", field as ActionsFilterField, value)}
onReset={(field) => resetTabFilter("actions", field as ActionsFilterField)}
onResetAll={() => resetAllTabFilters("actions")}
/>
Show PR runs
</label>
<FilterChips
groups={actionsFilterGroups}
values={viewState.tabFilters.actions}
onChange={(field, value) => setTabFilter("actions", field as ActionsFilterField, value)}
onReset={(field) => resetTabFilter("actions", field as ActionsFilterField)}
onResetAll={() => resetAllTabFilters("actions")}
/>
<div class="flex-1" />
<ExpandCollapseButtons
onExpandAll={() => setAllExpanded("actions", repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded("actions", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "workflowRun")}
onUnignore={unignoreItem}
/>
</div>
<div class="shrink-0 flex items-center gap-2 py-0.5">
<ExpandCollapseButtons
onExpandAll={() => setAllExpanded("actions", repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded("actions", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "workflowRun")}
onUnignore={unignoreItem}
/>
</div>
</div>

{/* Loading skeleton — only when no data exists yet */}
Expand Down
37 changes: 32 additions & 5 deletions src/app/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,38 @@ export default function DashboardPage() {

const refreshTick = createMemo(() => (dashboardData.lastRefreshedAt?.getTime() ?? 0) + clockTick());

const tabCounts = createMemo(() => ({
issues: dashboardData.issues.length,
pullRequests: dashboardData.pullRequests.length,
actions: dashboardData.workflowRuns.length,
}));
const tabCounts = createMemo(() => {
const { org, repo } = viewState.globalFilter;
const ignoredByType = (type: string) =>
new Set(viewState.ignoredItems.filter((i) => i.type === type).map((i) => i.id));

const ignoredIssues = ignoredByType("issue");
const ignoredPRs = ignoredByType("pullRequest");
const ignoredRuns = ignoredByType("workflowRun");

return {
issues: dashboardData.issues.filter((i) => {
if (ignoredIssues.has(String(i.id))) return false;
if (viewState.hideDepDashboard && i.title === "Dependency Dashboard") return false;
if (repo && i.repoFullName !== repo) return false;
if (org && !i.repoFullName.startsWith(org + "/")) return false;
return true;
}).length,
pullRequests: dashboardData.pullRequests.filter((p) => {
if (ignoredPRs.has(String(p.id))) return false;
if (repo && p.repoFullName !== repo) return false;
if (org && !p.repoFullName.startsWith(org + "/")) return false;
return true;
}).length,
actions: dashboardData.workflowRuns.filter((w) => {
if (ignoredRuns.has(String(w.id))) return false;
if (!viewState.showPrRuns && w.isPrRun) return false;
if (repo && w.repoFullName !== repo) return false;
if (org && !w.repoFullName.startsWith(org + "/")) return false;
return true;
}).length,
};
});

const userLogin = createMemo(() => user()?.login ?? "");
const allUsers = createMemo(() => {
Expand Down
90 changes: 47 additions & 43 deletions src/app/components/dashboard/IssuesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export default function IssuesTab(props: IssuesTabProps) {
const highlightedReposIssues = createReorderHighlight(
() => repoGroups().map(g => g.repoFullName),
() => viewState.lockedRepos.issues,
() => viewState.ignoredItems.filter(i => i.type === "issue").length,
);

function handleSort(field: string, direction: "asc" | "desc") {
Expand All @@ -223,49 +224,52 @@ export default function IssuesTab(props: IssuesTabProps) {
return (
<div class="flex flex-col h-full">
{/* Sort dropdown + filter chips + ignore badge toolbar */}
<div class="flex flex-wrap items-center gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
<SortDropdown
options={sortOptions}
value={sortPref().field}
direction={sortPref().direction}
onChange={handleSort}
/>
<FilterChips
groups={filterGroups()}
values={viewState.tabFilters.issues}
onChange={(field, value) => {
setTabFilter("issues", field as IssueFilterField, value);
setPage(0);
}}
onReset={(field) => {
resetTabFilter("issues", field as IssueFilterField);
setPage(0);
}}
onResetAll={() => {
resetAllTabFilters("issues");
setPage(0);
}}
/>
<button
onClick={() => {
updateViewState({ hideDepDashboard: !viewState.hideDepDashboard });
setPage(0);
}}
class={`btn btn-xs rounded-full ${!viewState.hideDepDashboard ? "btn-primary" : "btn-ghost text-base-content/50"}`}
aria-pressed={!viewState.hideDepDashboard}
title="Toggle visibility of Dependency Dashboard issues"
>
Show Dep Dashboard
</button>
<div class="flex-1" />
<ExpandCollapseButtons
onExpandAll={() => setAllExpanded("issues", repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded("issues", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "issue")}
onUnignore={unignoreItem}
/>
<div class="flex items-start gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
<div class="flex flex-wrap items-center gap-3 min-w-0 flex-1">
<SortDropdown
options={sortOptions}
value={sortPref().field}
direction={sortPref().direction}
onChange={handleSort}
/>
<FilterChips
groups={filterGroups()}
values={viewState.tabFilters.issues}
onChange={(field, value) => {
setTabFilter("issues", field as IssueFilterField, value);
setPage(0);
}}
onReset={(field) => {
resetTabFilter("issues", field as IssueFilterField);
setPage(0);
}}
onResetAll={() => {
resetAllTabFilters("issues");
setPage(0);
}}
/>
<button
onClick={() => {
updateViewState({ hideDepDashboard: !viewState.hideDepDashboard });
setPage(0);
}}
class={`btn btn-xs rounded-full ${!viewState.hideDepDashboard ? "btn-primary" : "btn-ghost text-base-content/50"}`}
aria-pressed={!viewState.hideDepDashboard}
title="Toggle visibility of Dependency Dashboard issues"
>
Show Dep Dashboard
</button>
</div>
<div class="shrink-0 flex items-center gap-2 py-0.5">
<ExpandCollapseButtons
onExpandAll={() => setAllExpanded("issues", repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded("issues", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "issue")}
onUnignore={unignoreItem}
/>
</div>
</div>

{/* Loading skeleton — only when no data exists yet */}
Expand Down
68 changes: 36 additions & 32 deletions src/app/components/dashboard/PullRequestsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
const highlightedReposPRs = createReorderHighlight(
() => repoGroups().map(g => g.repoFullName),
() => viewState.lockedRepos.pullRequests,
() => viewState.ignoredItems.filter(i => i.type === "pullRequest").length,
);

function handleSort(field: string, direction: "asc" | "desc") {
Expand All @@ -321,38 +322,41 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
return (
<div class="flex flex-col h-full">
{/* Filter toolbar with SortDropdown */}
<div class="flex flex-wrap items-center gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
<SortDropdown
options={sortOptions}
value={sortPref().field}
direction={sortPref().direction}
onChange={handleSort}
/>
<FilterChips
groups={filterGroups()}
values={viewState.tabFilters.pullRequests}
onChange={(field, value) => {
setTabFilter("pullRequests", field as PullRequestFilterField, value);
setPage(0);
}}
onReset={(field) => {
resetTabFilter("pullRequests", field as PullRequestFilterField);
setPage(0);
}}
onResetAll={() => {
resetAllTabFilters("pullRequests");
setPage(0);
}}
/>
<div class="flex-1" />
<ExpandCollapseButtons
onExpandAll={() => setAllExpanded("pullRequests", repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded("pullRequests", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "pullRequest")}
onUnignore={unignoreItem}
/>
<div class="flex items-start gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
<div class="flex flex-wrap items-center gap-3 min-w-0 flex-1">
<SortDropdown
options={sortOptions}
value={sortPref().field}
direction={sortPref().direction}
onChange={handleSort}
/>
<FilterChips
groups={filterGroups()}
values={viewState.tabFilters.pullRequests}
onChange={(field, value) => {
setTabFilter("pullRequests", field as PullRequestFilterField, value);
setPage(0);
}}
onReset={(field) => {
resetTabFilter("pullRequests", field as PullRequestFilterField);
setPage(0);
}}
onResetAll={() => {
resetAllTabFilters("pullRequests");
setPage(0);
}}
/>
</div>
<div class="shrink-0 flex items-center gap-2 py-0.5">
<ExpandCollapseButtons
onExpandAll={() => setAllExpanded("pullRequests", repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded("pullRequests", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "pullRequest")}
onUnignore={unignoreItem}
/>
</div>
</div>

{/* Loading skeleton — only when no data exists yet */}
Expand Down
7 changes: 6 additions & 1 deletion src/app/lib/reorderHighlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@ import { detectReorderedRepos } from "./grouping";
export function createReorderHighlight(
getRepoOrder: Accessor<string[]>,
getLockedOrder: Accessor<string[]>,
getIgnoredCount: Accessor<number>,
): Accessor<ReadonlySet<string>> {
let prevOrder: string[] = [];
let prevLocked: string[] = [];
let prevIgnoredCount = getIgnoredCount();
let timeout: ReturnType<typeof setTimeout> | undefined;
const [highlighted, setHighlighted] = createSignal<ReadonlySet<string>>(new Set());

createEffect(() => {
const currentOrder = getRepoOrder();
const currentLocked = getLockedOrder();
const currentIgnoredCount = getIgnoredCount();

const lockedChanged = currentLocked.length !== prevLocked.length
|| currentLocked.some((r, i) => r !== prevLocked[i]);
const ignoredChanged = currentIgnoredCount !== prevIgnoredCount;

if (prevOrder.length > 0 && !lockedChanged) {
if (prevOrder.length > 0 && !lockedChanged && !ignoredChanged) {
const moved = detectReorderedRepos(prevOrder, currentOrder);
if (moved.size > 0) {
setHighlighted(moved);
Expand All @@ -28,6 +32,7 @@ export function createReorderHighlight(

prevOrder = currentOrder;
prevLocked = [...currentLocked];
prevIgnoredCount = currentIgnoredCount;
});
onCleanup(() => clearTimeout(timeout));

Expand Down
Loading