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
5 changes: 3 additions & 2 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ The tab uses a multi-layer detection pipeline to identify dependency PRs:
4. **Title pattern** — PR titles matching common dependency update patterns (e.g., "Bump X from Y to Z", "chore(deps): ...", "[Snyk] ...") are detected.
5. **Label match** — PRs with the `dependencies` label are included.

Dependency PRs claimed by the Dependencies tab are excluded from the standard Pull Requests tab and any custom tabs with exclusivity enabled. The tab title shows the current count of open dependency PRs.
Dependency PRs claimed by the Dependencies tab are excluded from the standard Pull Requests tab and any custom tabs with exclusivity enabled. The tab title shows the current count of open dependency PRs. This exclusivity still applies to repos you've excluded from the Dependencies tab — their dependency PRs disappear entirely rather than reappearing in Pull Requests.

### Status Grouping

Expand All @@ -307,7 +307,7 @@ Within each group, PRs are sorted by repository name, then update category (main

If a Renovate Dashboard issue is detected in one of your tracked repos, abandoned dependency entries from its "Abandoned" section are shown as pill badges on matching PR rows. Each pill links directly to the Renovate Dashboard issue so you can investigate further.

The parser reads the Renovate Dashboard issue body to extract package names from the abandoned dependencies table.
The parser reads the Renovate Dashboard issue body to extract package names from the abandoned dependencies table. Repos excluded from the Dependencies tab (see Dependencies Settings below) are skipped for abandoned-package detection too — their Renovate Dashboard issue is never checked.

### Dependencies Settings

Expand All @@ -317,6 +317,7 @@ Go to **Settings > Dependencies** to configure:
|---------|---------|-------------|
| Enable Dependencies tab | On | Show or hide the tab. When disabled, dependency PRs appear in the standard Pull Requests tab. |
| Rebase label | `rebase` | PRs with this label are shown with a "Rebasing" indicator in the Dependencies tab. Change to match the label name your dependency bot uses to signal rebase-needed status. |
| Excluded repos/orgs | (none) | Hide specific repos or entire orgs from the Dependencies tab — including their Renovate Dashboard "Abandoned" package badges. Picked from your selected, upstream, and monitored repos. Excluding an org covers all repos under it, including ones added later. Regular issues, pull requests, and workflow runs for excluded repos are unaffected — only their dependency-bot PRs are hidden, and those don't reappear in Pull Requests either. |

### Dependencies Filters

Expand Down
26 changes: 20 additions & 6 deletions src/app/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { config, setConfig, getCustomTab, isBuiltinTab, isActionsBasedTab, isTab
import { viewState, updateViewState, setSortPreference, pruneClosedTrackedItems, removeCustomTabState, untrackJiraItem, setTabFilter, IssueFiltersSchema, PullRequestFiltersSchema, ActionsFiltersSchema } from "../../stores/view";
import DependenciesTab from "./DependenciesTab";
import { isDependencyPr, expandBotLogins, needsBodyFallback, parseRenovateBody, type VersionInfo } from "../../lib/dependency-detection";
import { isRepoExcludedFromDependencies } from "../../lib/dependency-exclusion";
import { findDashboardIssues, parseAbandonedSection, resetAbandonedPatternCache, type AbandonedDependency } from "../../lib/dependency-dashboard";
import { fetchDashboardIssueBodies, fetchDepPRBodies } from "../../services/api";
import type { SortOption } from "../shared/SortDropdown";
Expand Down Expand Up @@ -857,10 +858,22 @@ export default function DashboardPage() {
const trackedBotLogins = createMemo(() =>
expandBotLogins(config.trackedUsers.filter((u) => u.type === "bot").map((u) => u.login.toLowerCase()))
);
// Unfiltered classification — for rendering/counting use visibleDependencyPullRequests below.
const dependencyPullRequests = createMemo(() => {
if (!config.dependencies.enabled) return [];
return dashboardData.pullRequests.filter((pr) => pr.state === "OPEN" && isDependencyPr(pr, trackedBotLogins()));
});

// Dependencies-tab display filter — applied on top of the unfiltered
// dependencyPullRequests classification above. dependencyPrIds (below) and
// exclusiveOwnership deliberately keep using the UNFILTERED memo, so an
// excluded repo's bot PRs still don't leak into the main Pull Requests tab —
// they vanish entirely instead of reappearing elsewhere.
const visibleDependencyPullRequests = createMemo(() =>
dependencyPullRequests().filter(
(pr) => !isRepoExcludedFromDependencies(pr.repoFullName, config.dependencies.excludedOrgs, config.dependencies.excludedRepos)
)
);
const dependencyPrIds = createMemo(() =>
new Set(dependencyPullRequests().map((pr) => pr.id))
);
Expand Down Expand Up @@ -917,7 +930,7 @@ export default function DashboardPage() {
}

const enableDependencies = createMemo(() =>
config.dependencies.enabled && dependencyPullRequests().length > 0
config.dependencies.enabled && visibleDependencyPullRequests().length > 0
);

// Visible data for built-in tabs — filters out exclusively-owned items
Expand Down Expand Up @@ -1070,7 +1083,7 @@ export default function DashboardPage() {
return true;
}).length };
})() : {}),
...(enableDependencies() ? { dependencies: dependencyPullRequests().filter((p) => !ignoredPRs.has(p.id)).length } : {}),
...(enableDependencies() ? { dependencies: visibleDependencyPullRequests().filter((p) => !ignoredPRs.has(p.id)).length } : {}),
...customCounts,
};
});
Expand Down Expand Up @@ -1163,7 +1176,7 @@ export default function DashboardPage() {
void (async () => {
try {
const dashboardIssues = findDashboardIssues(dashboardData.issues, trackedBotLogins());
const depRepos = new Set(dependencyPullRequests().map((pr) => pr.repoFullName));
const depRepos = new Set(visibleDependencyPullRequests().map((pr) => pr.repoFullName));
const relevant = dashboardIssues.filter((di) => depRepos.has(di.repoFullName));
if (relevant.length === 0) return;

Expand Down Expand Up @@ -1202,7 +1215,8 @@ export default function DashboardPage() {

const meta = depMeta();
const depPrs = dependencyPullRequests();
const toFetch = depPrs.filter((pr) => !meta.has(pr.id) && needsBodyFallback(pr));
const visibleDepPrs = visibleDependencyPullRequests();
const toFetch = visibleDepPrs.filter((pr) => !meta.has(pr.id) && needsBodyFallback(pr));
if (toFetch.length === 0) return;

_fetchingDepBodies = true;
Expand Down Expand Up @@ -1323,7 +1337,7 @@ export default function DashboardPage() {
</Match>
<Match when={activeTab() === "dependencies"}>
<DependenciesTab
pullRequests={dependencyPullRequests()}
pullRequests={visibleDependencyPullRequests()}
depMeta={depMeta()}
loading={dashboardData.loading}
abandonedDepsMap={abandonedDepsMap()}
Expand All @@ -1338,7 +1352,7 @@ export default function DashboardPage() {
</Match>
<Match when={activeTab() === "dependencies"}>
<DependenciesTab
pullRequests={dependencyPullRequests()}
pullRequests={visibleDependencyPullRequests()}
loading={dashboardData.loading}
abandonedDepsMap={abandonedDepsMap()}
dashboardIssueUrls={dashboardIssueUrls()}
Expand Down
94 changes: 94 additions & 0 deletions src/app/components/settings/DependencyExclusionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Dialog } from "@kobalte/core/dialog";
import type { RepoRef } from "../../services/api";
import { createOrgRepoSelection } from "../../lib/orgRepoSelection";
import OrgRepoCheckboxTree from "../shared/OrgRepoCheckboxTree";

interface DependencyExclusionModalProps {
open: boolean;
onClose: () => void;
availableOrgs: string[];
availableRepos: RepoRef[];
excludedOrgs: string[];
excludedRepos: RepoRef[];
onSave: (excludedOrgs: string[], excludedRepos: RepoRef[]) => void;
}

export default function DependencyExclusionModal(props: DependencyExclusionModalProps) {
const {
selectedOrgs: excludedOrgs,
selectedRepos: excludedRepos,
toggleOrg,
toggleRepo,
buildRepoList: buildExcludedRepos,
} = createOrgRepoSelection({
getOpen: () => props.open,
getAvailableRepos: () => props.availableRepos,
getInitialOrgs: () => props.excludedOrgs,
getInitialRepos: () => props.excludedRepos.map((r) => r.fullName),
});

function handleSave() {
props.onSave([...excludedOrgs()], buildExcludedRepos());
props.onClose();
}

return (
<Dialog open={props.open} onOpenChange={(open) => !open && props.onClose()} modal>
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 bg-black/50 z-[70]" />
<Dialog.Content class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg bg-base-100 rounded-xl shadow-xl z-[71] flex flex-col max-h-[90vh]">
<Dialog.Description class="sr-only">
Manage repos and orgs excluded from the Dependencies tab
</Dialog.Description>

{/* Header */}
<div class="flex items-center gap-2 px-5 py-4 border-b border-base-300 shrink-0">
<Dialog.Title class="text-lg font-semibold flex-1">
Exclude from Dependencies
</Dialog.Title>
<button
type="button"
class="btn btn-ghost btn-sm btn-circle"
aria-label="Close"
onClick={props.onClose}
>
</button>
</div>

{/* Scrollable body */}
<div class="overflow-y-auto flex-1 px-5 py-4 space-y-3">
<p class="text-xs text-base-content/50">
Repos and orgs checked below are hidden from the Dependencies tab only — their dependency-bot PRs won't reappear in Pull Requests, but regular issues, pull requests, and workflow runs are unaffected.
</p>
<div class="space-y-3">
<OrgRepoCheckboxTree
availableOrgs={props.availableOrgs}
availableRepos={props.availableRepos}
checkedOrgs={excludedOrgs()}
checkedRepos={excludedRepos()}
onToggleOrg={toggleOrg}
onToggleRepo={toggleRepo}
emptyMessage="No repos tracked yet."
/>
</div>
</div>

{/* Footer */}
<div class="flex items-center justify-end gap-2 px-5 py-4 border-t border-base-300 shrink-0">
<button type="button" class="btn btn-ghost btn-sm" onClick={props.onClose}>
Cancel
</button>
<button
type="button"
class="btn btn-primary btn-sm"
onClick={handleSave}
>
Save
</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
);
}
50 changes: 49 additions & 1 deletion src/app/components/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { pushNotification } from "../../lib/errors";
import { buildOrgAccessUrl, buildJiraAuthorizeUrl } from "../../lib/oauth";
import { sealApiToken } from "../../lib/proxy";
import { isSafeGitHubUrl, openGitHubUrl } from "../../lib/url";
import { relativeTime } from "../../lib/format";
import { relativeTime, formatScopeSummary } from "../../lib/format";
import { fetchOrgs } from "../../services/api";
import { getClient } from "../../services/github";
import { getUsageSnapshot, getUsageResetAt, resetUsageData, checkAndResetIfExpired, SOURCE_LABELS } from "../../services/api-usage";
Expand All @@ -27,6 +27,7 @@ import ThemePicker from "./ThemePicker";
import DensityPicker from "./DensityPicker";
import TrackedUsersSection from "./TrackedUsersSection";
import CustomTabsSection from "./CustomTabsSection";
import DependencyExclusionModal from "./DependencyExclusionModal";
import { InfoTooltip } from "../shared/Tooltip";
import { createJiraClient } from "../../lib/jira-utils";
import JiraFieldPicker from "./JiraFieldPicker";
Expand Down Expand Up @@ -142,6 +143,21 @@ export default function SettingsPage() {
config.monitoredRepos.map(r => r.fullName).join(", ")
);

const dependencyExclusionPool = createMemo(() => {
const seen = new Map<string, RepoRef>();
for (const r of [...config.selectedRepos, ...config.upstreamRepos, ...config.monitoredRepos]) {
seen.set(r.fullName.toLowerCase(), r);
}
return [...seen.values()];
});
const dependencyExclusionOrgs = createMemo(() =>
[...new Set(dependencyExclusionPool().map((r) => r.owner))]
);
const excludedCounts = createMemo(() => ({
orgs: (config.dependencies?.excludedOrgs ?? []).length,
repos: (config.dependencies?.excludedRepos ?? []).length,
}));

// ── Helpers ──────────────────────────────────────────────────────────────

async function mergeNewOrgs() {
Expand Down Expand Up @@ -352,6 +368,7 @@ export default function SettingsPage() {
const [jiraApiMode, setJiraApiMode] = createSignal(false);
const [showFieldPicker, setShowFieldPicker] = createSignal(false);
const [showScopePicker, setShowScopePicker] = createSignal(false);
const [showDependencyExclusionModal, setShowDependencyExclusionModal] = createSignal(false);

const jiraClient = createMemo(() => createJiraClient(config.jira?.authMethod));

Expand Down Expand Up @@ -1356,6 +1373,25 @@ export default function SettingsPage() {
onInput={(e) => saveWithFeedback({ dependencies: { ...config.dependencies, rebaseLabel: e.currentTarget.value || "rebase" } })}
/>
</SettingRow>
<SettingRow
label="Excluded repos/orgs"
description="Hide specific repos or entire orgs from the Dependencies tab only — their dependency-bot PRs won't reappear in Pull Requests, but regular issues, pull requests, and workflow runs for those repos are unaffected"
>
<div class="flex items-center gap-3">
<span class="text-xs text-base-content/60">
{excludedCounts().orgs === 0 && excludedCounts().repos === 0
? "None excluded"
: formatScopeSummary(excludedCounts().orgs, excludedCounts().repos, true)}
</span>
<button
type="button"
class="btn btn-sm btn-outline"
onClick={() => setShowDependencyExclusionModal(true)}
>
Manage
</button>
</div>
</SettingRow>
</Section>

{/* ── Account ─────────────────────────────────────────────────── */}
Expand Down Expand Up @@ -1539,6 +1575,18 @@ export default function SettingsPage() {
</div>
</div>

<DependencyExclusionModal
open={showDependencyExclusionModal()}
onClose={() => setShowDependencyExclusionModal(false)}
availableOrgs={dependencyExclusionOrgs()}
availableRepos={dependencyExclusionPool()}
excludedOrgs={config.dependencies?.excludedOrgs ?? []}
excludedRepos={config.dependencies?.excludedRepos ?? []}
onSave={(orgs, repos) =>
saveWithFeedback({ dependencies: { ...config.dependencies, excludedOrgs: orgs, excludedRepos: repos } })
}
/>

<footer class="mt-8 border-t border-base-300 pt-4 pb-8 text-xs text-base-content/50 text-center">
<div class="flex items-center justify-center gap-3">
<a
Expand Down
Loading