diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 21d14ee5..5142eda2 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -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 @@ -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 @@ -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 diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 6b96ad1b..99be3529 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -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"; @@ -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)) ); @@ -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 @@ -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, }; }); @@ -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; @@ -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; @@ -1323,7 +1337,7 @@ export default function DashboardPage() { 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 ( + !open && props.onClose()} modal> + + + + + Manage repos and orgs excluded from the Dependencies tab + + + {/* Header */} +
+ + Exclude from Dependencies + + +
+ + {/* Scrollable body */} +
+

+ 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. +

+
+ +
+
+ + {/* Footer */} +
+ + +
+
+
+
+ ); +} diff --git a/src/app/components/settings/SettingsPage.tsx b/src/app/components/settings/SettingsPage.tsx index 0cad0b09..3febd677 100644 --- a/src/app/components/settings/SettingsPage.tsx +++ b/src/app/components/settings/SettingsPage.tsx @@ -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"; @@ -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"; @@ -142,6 +143,21 @@ export default function SettingsPage() { config.monitoredRepos.map(r => r.fullName).join(", ") ); + const dependencyExclusionPool = createMemo(() => { + const seen = new Map(); + 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() { @@ -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)); @@ -1356,6 +1373,25 @@ export default function SettingsPage() { onInput={(e) => saveWithFeedback({ dependencies: { ...config.dependencies, rebaseLabel: e.currentTarget.value || "rebase" } })} /> + +
+ + {excludedCounts().orgs === 0 && excludedCounts().repos === 0 + ? "None excluded" + : formatScopeSummary(excludedCounts().orgs, excludedCounts().repos, true)} + + +
+
{/* ── Account ─────────────────────────────────────────────────── */} @@ -1539,6 +1575,18 @@ export default function SettingsPage() { + 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 } }) + } + /> +