diff --git a/.changeset/fn-7971-hide-gitlab-when-disabled.md b/.changeset/fn-7971-hide-gitlab-when-disabled.md new file mode 100644 index 0000000000..e6ac3de60b --- /dev/null +++ b/.changeset/fn-7971-hide-gitlab-when-disabled.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Hide the GitLab import tab when GitLab integration is disabled in settings. +category: fix +dev: Gates GitHubImportModal.tsx GitLab provider visibility on effective gitlabEnabled and coerces disabled persisted state to GitHub. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index f2f15c7642..4e2e3bcba2 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -307,7 +307,7 @@ Use Import Tasks on desktop/tablet: 5. Select the import action. Expected outcome: Fusion creates a task (or review task for a pull request) on the board and preserves GitHub provenance/tracking metadata. After a successful issue import, the issue selection clears and the view returns to the main issue list/no-selection preview so completed issue actions do not leave stale buttons active. -Leaving and returning to **Import Tasks** (for example switching to Board and back) restores the prior context for the current project — provider (GitHub/GitLab), active Issues/PRs tab, label filter, selected repository/remote, GitLab project/group inputs, and the previously selected issue/PR — instead of resetting to defaults. The restored selection re-validates against the freshly reloaded list; a selection that no longer exists (e.g. the issue was closed upstream) clears gracefully rather than showing a stuck or empty preview. First-time opens with no prior state keep the existing default-remote auto-detect behavior. State is scoped per project and does not leak across projects. +Leaving and returning to **Import Tasks** (for example switching to Board and back) restores the prior context for the current project — provider (GitHub/GitLab), active Issues/PRs tab, label filter, selected repository/remote, GitLab project/group inputs, and the previously selected issue/PR — instead of resetting to defaults. When GitLab integration is disabled in Settings, the GitLab provider tab is hidden and any restored GitLab provider preference opens on GitHub instead; saved GitLab URLs and tokens remain configured. The restored selection re-validates against the freshly reloaded list; a selection that no longer exists (e.g. the issue was closed upstream) clears gracefully rather than showing a stuck or empty preview. First-time opens with no prior state keep the existing default-remote auto-detect behavior. State is scoped per project and does not leak across projects. Use GitHub import on mobile: @@ -1171,7 +1171,7 @@ Features: GitLab settings are collapsed by default to keep Settings less noisy. Use **Settings → Project → General → GitLab Configuration** for project GitLab URL/API overrides, **Settings → Project → Merge → GitLab Authentication** for project token settings, and **Settings → Global General → GitLab Configuration** for global fallbacks. Each disclosure header includes **Enable GitLab integration** so operators can disable GitLab without expanding advanced fields. -When `gitlabEnabled` is off, Fusion keeps saved GitLab URLs and tokens intact but disables outbound GitLab API work: Import Tasks GitLab fetch/import controls show an enable-in-Settings message, API/CLI/pi import paths reject before network calls, and lifecycle comments/close/reconcile/refresh paths skip with diagnostics. Existing imported-task GitLab metadata remains viewable. GitHub imports and GitHub settings are unchanged. GitLab Signals inbound webhooks are configured separately by `FUSION_SIGNAL_GITLAB_SECRET`; they are not governed by the outbound GitLab API enable toggle. +When `gitlabEnabled` is off, Fusion keeps saved GitLab URLs and tokens intact but disables outbound GitLab API work: the Import Tasks GitLab provider tab is hidden and restored GitLab import state opens on GitHub instead, API/CLI/pi import paths reject before network calls, and lifecycle comments/close/reconcile/refresh paths skip with diagnostics. Existing imported-task GitLab metadata remains viewable. GitHub imports and GitHub settings are unchanged. GitLab Signals inbound webhooks are configured separately by `FUSION_SIGNAL_GITLAB_SECRET`; they are not governed by the outbound GitLab API enable toggle. - **Signals** is backed by the project-scoped `/api/command-center/signals` endpoint, which aggregates real rows from the local `incidents` table. Verified external connectors (`POST /api/signals/gitlab`, `/webhook`, `/sentry`, `/datadog`, and `/pagerduty`) create triage tasks and also write/resolve incidents, so Signals shows total/open/resolved counts, MTTR when resolved incidents have enough timestamps, and source/severity/status breakdowns from connector traffic. GitLab supports GitLab.com and self-managed project/group issue and merge-request webhooks through the environment-only `FUSION_SIGNAL_GITLAB_SECRET` and `X-Gitlab-Token` header; no GitLab CLI or server-side link fetch is used. Signals adds an open-vs-resolved status pie from the same response. Signals has no per-day series today, so it intentionally does not render a line chart or fabricate a trend. The companion `/api/command-center/signals/connectors` endpoint returns only per-provider configured booleans, allowing the empty state to distinguish "no connector configured" from "connector configured, awaiting signals" without exposing secrets. - **System** is the canonical system-telemetry destination. It reads local telemetry from `GET /api/system-stats` and, when multiple registered nodes exist, shows a node selector that can proxy the same system-stats payload through `GET /api/nodes/:id/system-stats` for remote nodes. It renders live radial gauges for app CPU, host memory, and heap usage, keeps a small client-side rolling buffer for CPU/memory/heap trend sparklines, adds a recharts CPU/memory/heap line from that same rolling buffer, and adds a task-by-column pie alongside the existing tasks-by-column and agents-by-state bars. Host memory uses OS-available memory (Node `process.availableMemory()` when available, with a flagged `freemem` fallback) so macOS inactive/cache pages are not reported as used. The Vitest process count, manual kill confirmation, auto-kill toggle, threshold controls, and last-auto-kill timestamp moved here unchanged; the standalone System Stats modal and its desktop Header/mobile More affordances were removed. diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx index 33aafd79fc..65ae2e0924 100644 --- a/packages/dashboard/app/components/GitHubImportModal.tsx +++ b/packages/dashboard/app/components/GitHubImportModal.tsx @@ -338,6 +338,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, const [gitlabItems, setGitlabItems] = useState([]); const [selectedGitlabKey, setSelectedGitlabKey] = useState(null); const [gitlabEnabled, setGitlabEnabled] = useState(true); + const [gitlabSettingsLoaded, setGitlabSettingsLoaded] = useState(false); // Tab state const [activeTab, setActiveTab] = useState("issues"); @@ -693,12 +694,26 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, useEffect(() => { let cancelled = false; if (!isOpen) return () => { cancelled = true; }; + setGitlabSettingsLoaded(false); fetchSettings(projectId, { forceFresh: true }) .then((settings) => { - if (!cancelled) setGitlabEnabled(settings.gitlabEnabled !== false); + if (cancelled) return; + const resolvedGitlabEnabled = settings.gitlabEnabled !== false; + setGitlabEnabled(resolvedGitlabEnabled); + setGitlabSettingsLoaded(true); + if (!resolvedGitlabEnabled) { + setProvider("github"); + setGitlabItems([]); + setSelectedGitlabKey(null); + pendingRestoreSelectionRef.current.gitlabKey = null; + needsGitlabAutoLoadRef.current = false; + } }) .catch(() => { - if (!cancelled) setGitlabEnabled(true); + if (!cancelled) { + setGitlabEnabled(true); + setGitlabSettingsLoaded(true); + } }); return () => { cancelled = true; }; }, [isOpen, projectId]); @@ -774,15 +789,19 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, (project for project_issue/merge_request, group for group_issue), trigger exactly one auto-load so the restored selection has a list to be validated/re-applied against (see handleLoadGitLab). One-shot via the ref flag so manual reloads/tab switches never re-trigger this. + + FNXC:GitLabImportVisibility 2026-07-15-00:00: + FN-7971 changed disabled GitLab from visible-but-disabled to hidden. Wait for effective settings before replaying a persisted GitLab auto-load so `gitlabEnabled === false` can coerce the provider back to GitHub without firing a GitLab request. */ useEffect(() => { if (!needsGitlabAutoLoadRef.current) return; + if (!gitlabSettingsLoaded || !gitlabEnabled) return; if (provider !== "gitlab") return; const ready = gitlabResource === "group_issue" ? Boolean(gitlabGroup.trim()) : Boolean(gitlabProject.trim()); if (!ready) return; needsGitlabAutoLoadRef.current = false; handleLoadGitLab(); - }, [provider, gitlabResource, gitlabProject, gitlabGroup, handleLoadGitLab]); + }, [provider, gitlabSettingsLoaded, gitlabEnabled, gitlabResource, gitlabProject, gitlabGroup, handleLoadGitLab]); /* FNXC:GitHubImport 2026-07-07-00:00: @@ -1311,7 +1330,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
- + {gitlabEnabled ? : null}
{provider === "github" ? ( <> diff --git a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx index ac623fd67b..32cc22a327 100644 --- a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx @@ -268,21 +268,71 @@ describe("GitHubImportModal", () => { expect(onImport).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-099" })); }); - it("shows disabled GitLab import controls without fetching when GitLab is off", async () => { + it("hides the GitLab import provider and keeps GitHub active when GitLab is off", async () => { vi.mocked(fetchGitRemotes).mockResolvedValueOnce([]); vi.mocked(fetchSettings).mockResolvedValueOnce({ gitlabEnabled: false } as never); render(); - fireEvent.click(await screen.findByRole("button", { name: "GitLab" })); - expect(await screen.findByTestId("gitlab-import-disabled")).toHaveTextContent("GitLab integration disabled"); - expect(screen.getByRole("button", { name: /Load/ })).toBeDisabled(); - expect(screen.getByLabelText("GitLab project path or ID")).toBeDisabled(); - expect(screen.getByRole("tab", { name: "Group issues" })).toBeDisabled(); + await waitFor(() => { + expect(screen.queryByRole("button", { name: "GitLab" })).toBeNull(); + }); + expect(screen.getByRole("button", { name: "GitHub" })).toHaveAttribute("aria-pressed", "true"); + expect(screen.queryByTestId("gitlab-import-panel")).toBeNull(); + expect(screen.queryByTestId("gitlab-import-disabled")).toBeNull(); expect(apiFetchGitLabProjectIssues).not.toHaveBeenCalled(); expect(apiImportGitLabProjectIssue).not.toHaveBeenCalled(); }); + it("shows the GitLab import provider when GitLab is explicitly enabled", async () => { + vi.mocked(fetchGitRemotes).mockResolvedValueOnce([]); + vi.mocked(fetchSettings).mockResolvedValueOnce({ gitlabEnabled: true } as never); + + render(); + + expect(await screen.findByRole("button", { name: "GitLab" })).toBeInTheDocument(); + }); + + it("shows the GitLab import provider when the GitLab enabled setting is undefined", async () => { + vi.mocked(fetchGitRemotes).mockResolvedValueOnce([]); + vi.mocked(fetchSettings).mockResolvedValueOnce({} as never); + + render(); + + expect(await screen.findByRole("button", { name: "GitLab" })).toBeInTheDocument(); + }); + + it("coerces a persisted GitLab provider to GitHub without auto-loading when GitLab is off", async () => { + vi.mocked(fetchGitRemotes).mockResolvedValueOnce([]); + vi.mocked(fetchSettings).mockResolvedValueOnce({ gitlabEnabled: false } as never); + window.localStorage.setItem(`kb:project-1:${GITHUB_IMPORT_STATE_KEY}`, JSON.stringify({ + provider: "gitlab", + activeTab: "issues", + labels: "bug", + selectedRemoteName: "", + owner: "", + repo: "", + gitlabResource: "project_issue", + gitlabProject: "group/project", + gitlabGroup: "", + selectedIssueNumber: null, + selectedPullNumber: null, + selectedGitlabKey: "project_issue:3:2", + })); + + render(); + + await waitFor(() => { + expect(screen.queryByRole("button", { name: "GitLab" })).toBeNull(); + }); + expect(screen.getByRole("button", { name: "GitHub" })).toHaveAttribute("aria-pressed", "true"); + expect(screen.queryByTestId("gitlab-import-panel")).toBeNull(); + expect(screen.queryByTestId("gitlab-import-disabled")).toBeNull(); + expect(apiFetchGitLabProjectIssues).not.toHaveBeenCalled(); + expect(apiFetchGitLabGroupIssues).not.toHaveBeenCalled(); + expect(apiFetchGitLabMergeRequests).not.toHaveBeenCalled(); + }); + it("fetches group issues and merge requests without GitHub-only copy", async () => { vi.mocked(fetchGitRemotes).mockResolvedValue([]); vi.mocked(apiFetchGitLabGroupIssues).mockResolvedValueOnce([