Files
kandev/docs/specs/integrations/enable-disable-toggle.md
T
2026-08-11 19:34:52 +02:00

238 lines
12 KiB
Markdown

---
status: shipped
created: 2026-08-06
owner: platform
---
# Integration Enable/Disable Toggle & Nav Visibility
## Why
Every third-party integration (Azure DevOps, GitHub, GitLab, Jira, Linear,
Sentry) can be connected with credentials, but only Jira, Linear and Sentry
currently expose a user-facing on/off switch for their UI surfaces. Azure
DevOps, GitHub and GitLab have no such switch. Users who want to temporarily
silence an integration (e.g. while its credentials are being rotated, or
because a workspace doesn't use it) have no consistent, discoverable way to do
that across all six integrations, and no way to control whether a
disabled-but-still-configured integration continues to clutter the left panel
navigation. (Slack was retired from the core app to an external plugin after
this spec was written and is out of scope.)
## What
- Every integration (Azure DevOps, GitHub, GitLab, Jira, Linear, Sentry)
SHALL expose an enable/disable slider on its own settings page
(`/settings/integrations/<slug>`, and the per-workspace equivalent
`/settings/workspace/<id>/integrations/<slug>`). Jira, Linear and Sentry
already have this; Azure DevOps, GitHub and GitLab gain it.
- The same slider SHALL also appear on each integration's row/card on the
integrations index page (`/settings/integrations` and its per-workspace
equivalent), so a user can enable or disable any integration without
opening its detail page.
- Toggling the slider in either location SHALL keep both locations in sync
(same underlying per-integration enabled state, install-wide — not
per-workspace — consistent with the existing Jira/Linear/Sentry
toggle).
- The enabled state for Azure DevOps, GitHub and GitLab is purely a
presentation/navigation-visibility switch: it MUST NOT change whether their
existing PR/work-item/board/MR features function. This mirrors that those
three integrations have no existing UI surface gated on an "available"
concept beyond navigation and their own settings page banner.
- The integrations index page SHALL gain one new setting, **"Hide disabled
integrations from left panel navigation"**, disabled (off) by default.
- When that setting is **off** (default), a disabled-but-configured
integration MUST still appear in the left panel navigation (the sidebar's
Integrations section, its mobile-menu equivalent, and — whenever a tree
menu mode renders it; the default flat menu lists no per-workspace rows at
all — the Settings left panel's per-workspace Integrations list under
Settings → Workspaces → <workspace>) exactly as it does today for an
enabled integration — only credential/health status controls nav
visibility.
- When that setting is **on**, a disabled integration MUST be hidden from all
of those left-panel surfaces regardless of its credential/health status. An
enabled, healthy integration is unaffected. Only integrations whose
enable/disable toggle is off are hidden — an enabled-but-unconfigured
integration stays listed (the Settings tree keeps its own configured-status
badge convention).
- The new setting SHALL NOT change any other behavior gated on an
integration's existing "available" signal (e.g. Jira/Linear import
popovers, Kanban external-link buttons, task-top-bar issue buttons) — those
keep gating on enabled-and-authed exactly as they do today, independent of
left-panel nav visibility.
- All new/changed toggles persist and sync the same way the existing
Jira/Linear/Sentry "enabled" toggle does: a `localStorage`-backed,
install-wide boolean, synced across browser tabs, with a manual-save
affordance consistent with the rest of the Settings UI (`data-settings-dirty`
+ the shared save bar) where the control lives on a settings page.
## Data model
No new persistent (backend/database) state. All state introduced by this
feature lives in the browser's `localStorage`, matching the existing pattern
in `hooks/domains/integrations/use-integration-enabled.ts`:
| Key | Type | Default | Notes |
|---|---|---|---|
| `kandev:azure-devops:enabled:v1` | boolean | `true` | New. Mirrors `kandev:jira:enabled:v1`. |
| `kandev:github:enabled:v1` | boolean | `true` | New. |
| `kandev:gitlab:enabled:v1` | boolean | `true` | New. |
| `kandev:integrations:hideDisabledInNav:v1` | boolean | `false` | New. Not per-integration; one flag for the whole nav-filtering behavior. |
Existing keys (`kandev:jira:enabled:v1`, `kandev:linear:enabled:v1`,
`kandev:sentry:enabled:v1`) are unchanged.
## API surface
No backend/HTTP/WS changes. This is entirely a frontend feature.
Frontend primitives (new):
- `hooks/domains/azure-devops/use-azure-devops-enabled.ts` — exports
`useAzureDevOpsEnabled()`, a one-line wrapper over
`useIntegrationEnabled(storageKey, legacyKeyPrefix, syncEvent)`, mirroring
`hooks/domains/jira/use-jira-enabled.ts`. No legacy key prefix existed
before, so the legacy-migration argument is inert (no prior keys to
migrate) but kept for signature symmetry.
- `hooks/domains/github/use-github-enabled.ts` — same shape, for GitHub.
- `hooks/domains/gitlab/use-gitlab-enabled.ts` — same shape, for GitLab.
- `hooks/domains/integrations/use-hide-disabled-integrations-in-nav.ts` —
exports `useHideDisabledIntegrationsInNav()` returning
`{ hideDisabled: boolean; setHideDisabled: (next: boolean) => void }`,
backed by `localStorage` key `kandev:integrations:hideDisabledInNav:v1`
(default `false`), using the same `useSyncExternalStore` +
`storage`-event + custom-event-broadcast shape as
`use-integration-enabled.ts` (custom event name
`kandev:integrations:hide-disabled-in-nav-changed`), so nav consumers
re-render immediately in the same tab, and other tabs update via the
browser `storage` event.
Existing primitives reused unchanged:
- `hooks/domains/integrations/use-integration-enabled.ts` (`useIntegrationEnabled`)
- `hooks/domains/jira/use-jira-enabled.ts`, `use-linear-enabled.ts`,
`use-sentry-enabled.ts`
- `hooks/domains/jira/use-jira-availability.ts`'s `useJiraAuthed` and
`hooks/domains/linear/use-linear-availability.ts`'s `useLinearAuthed` —
these already expose the "configured and healthy, independent of the
enabled toggle" signal and are the building block for decoupled nav
filtering.
- `components/integrations/use-drafted-integration-enabled.ts` +
`components/integrations/drafted-integration-enabled-control.tsx` — the
existing draft/save-wired slider component
(`<DraftedIntegrationEnabledControl id={slug} enabled={enabled} persist={setEnabled} />`),
reused as-is for Azure DevOps/GitHub/GitLab's own-page slider (new
`<AzureDevOpsEnabledControl>`, `<GitHubEnabledControl>`,
`<GitLabEnabledControl>` wrapper components, one per integration,
mirroring `components/jira/jira-enabled-control.tsx`) and reused again for
the index-page per-row sliders (all six integrations) and for the new
"hide disabled in nav" row.
Modified:
- `hooks/use-nav-availability.ts`'s `useNavAvailability()` — see Scenarios
below for the exact filtering contract. Its return shape
(`AvailabilityMap`) is unchanged; only how each key's boolean is derived
changes.
## Permissions
No change. The toggle is a per-browser-profile, install-wide UI preference
with no authorization dimension (same as the existing Jira/Linear/Sentry
toggle).
## Failure modes
- `localStorage` unavailable or throwing (private browsing, quota) — the
existing `useIntegrationEnabled` implementation already degrades to
`enabled: true` and swallows write errors; the new hooks reuse this
behavior. `useHideDisabledIntegrationsInNav` degrades to `hideDisabled:
false` (its documented default) under the same failure, so a storage
failure never hides an integration a user can't otherwise see or control.
- No integration ever hides from a *settings* page based on the new nav
setting — `hideDisabled` affects only left-panel navigation surfaces
(`useNavAvailability`'s output for the main sidebar and mobile menu, and
the settings menu's visible-slug filter — `useVisibleIntegrationSlugs` in
`use-settings-menu-branches.ts` feeding `integrationNodes` — for the
Settings left panel's per-workspace Integrations branch), never a settings
route's reachability.
A user can always reach a disabled integration's own settings page
directly (by URL, from the index page, or from the sidebar's Settings
section) and re-enable it even while the left-panel nav entry is hidden.
## Persistence guarantees
State lives only in `localStorage`; it is not part of any backend restart or
task-execution durability guarantee, matching the existing enabled-toggle
convention. It survives a browser restart but not a `localStorage` clear, and
is not synced across devices/browsers.
## Scenarios
- **GIVEN** GitHub has valid, healthy credentials and the "hide disabled"
setting is off (default), **WHEN** the user disables GitHub via either
slider (own page or index-page row), **THEN** GitHub's sidebar Integrations
entry and mobile-menu entry remain visible (still reachable), while the
toggle itself reads "Disabled" on both the index-page row and the GitHub
settings page.
- **GIVEN** GitHub has valid, healthy credentials and is disabled, **WHEN**
the user turns on "Hide disabled integrations from left panel navigation"
on the integrations index page, **THEN** GitHub's sidebar Integrations
entry and mobile-menu entry disappear immediately (no reload required, no
change to GitHub's health/configured status).
- **GIVEN** the "hide disabled" setting is on and GitHub is disabled and
hidden from the nav, **WHEN** the user re-enables GitHub (either slider),
**THEN** GitHub's sidebar entry reappears immediately.
- **GIVEN** the "hide disabled" setting is on, **WHEN** an integration is
enabled but not configured/healthy (e.g. no credentials saved), **THEN**
it stays hidden from the nav exactly as it does today — the new setting
never *reveals* an unconfigured integration.
- **GIVEN** Jira is disabled and the "hide disabled" setting is on, **WHEN**
the user opens the Jira import popover on a task (an existing surface
gated on Jira's combined enabled+authed "available" signal), **THEN** the
import popover remains hidden exactly as it does today when Jira is
disabled — the new setting does not change that unrelated gate.
- **GIVEN** the integrations index page, **WHEN** it renders, **THEN** every
one of the six integration rows/cards shows its own enable/disable
slider reflecting that integration's current stored state, and toggling a
slider does not navigate to that integration's detail page.
- **GIVEN** the integrations index page and the Azure DevOps settings page
open in two browser tabs, **WHEN** the user disables Azure DevOps on the
index page in tab A, **THEN** tab B's Azure DevOps settings-page slider
updates to "Disabled" after the next `storage` event (existing
cross-tab-sync behavior, unchanged mechanism, extended to the three new
integrations).
## Out of scope
- No backend/database changes; the toggle and the nav-filter setting are
both pure frontend, `localStorage`-backed preferences.
- No change to Azure DevOps/GitHub/GitLab's PR, work-item, board, or MR
features when disabled — the new toggle for these three integrations only
controls settings-page/index-page slider state and left-panel nav
visibility, not functional gating (unlike Jira/Linear/Sentry's
existing toggle, which already gates other surfaces and is unchanged by
this feature).
- No change to the Settings-page navigation tree's status-badge convention
(`components/app-sidebar/sections/settings/workspaces-group.tsx`): its
per-workspace Integrations list keeps the configured-status badge
("Enabled" = credentials/health present), which is unrelated to the
enable/disable toggle. The "hide disabled" setting DOES filter that list
(a disabled integration is hidden from it when the setting is on), since
users reach it through left-panel navigation; the badge convention itself
is unchanged.
- Sentry has no main-sidebar nav destination today (see
`lib/navigation/core-destinations.ts` — only Azure DevOps, GitHub, GitLab,
Jira and Linear are nav-gated), so the setting has no effect for Sentry in
the main sidebar or mobile menu. Sentry DOES appear in the Settings left
panel's per-workspace Integrations tree (all six integrations are always
listed there), so with the setting on and Sentry's toggle off it is hidden
from that tree like any other disabled integration; its existing slider
(own page + new index-page row) still works for the enable/disable state
itself.
- No new command-palette entries or keyboard shortcuts for the new toggles.
## Open questions
(none)