FN-7971: hide GitLab import tab when GitLab is disabled

Hide the Import Tasks GitLab provider affordance when gitlabEnabled is off, coerce restored GitLab state to GitHub, and document the behavior.

- Gate GitLab provider tab visibility on effective gitlabEnabled and wait for settings before replaying persisted GitLab auto-load
- Coerce disabled GitLab provider preference to GitHub without firing GitLab fetch/import requests
- Cover hide/show/coerce paths in GitHubImportModal tests and update dashboard guide copy
- Add patch changeset for the published package

Files changed:
 .changeset/fn-7971-hide-gitlab-when-disabled.md    |  7 +++
 docs/dashboard-guide.md                            |  4 +-
 .../dashboard/app/components/GitHubImportModal.tsx | 27 ++++++++--
 .../__tests__/GitHubImportModal.test.tsx           | 62 +++++++++++++++++++---
 4 files changed, 88 insertions(+), 12 deletions(-)

Fusion-Task-Id: FN-7971

Fusion-Task-Lineage: e645a4a7-85e0-4635-8dad-f5839390e5c5

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-15 12:47:14 -07:00
parent 6e3a338cac
commit d893a026df
4 changed files with 88 additions and 12 deletions

View File

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

View File

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

View File

@@ -338,6 +338,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
const [gitlabItems, setGitlabItems] = useState<GitLabImportItem[]>([]);
const [selectedGitlabKey, setSelectedGitlabKey] = useState<string | null>(null);
const [gitlabEnabled, setGitlabEnabled] = useState(true);
const [gitlabSettingsLoaded, setGitlabSettingsLoaded] = useState(false);
// Tab state
const [activeTab, setActiveTab] = useState<TabType>("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,
<div className="modal-body github-import-modal__body">
<div className="github-import-provider" role="group" aria-label={t("git.providerAriaLabel", "Import provider")}>
<button type="button" className={`github-import-tab ${provider === "github" ? "active" : ""}`} aria-pressed={provider === "github"} onClick={() => setProvider("github")} disabled={loading || importing}>GitHub</button>
<button type="button" className={`github-import-tab ${provider === "gitlab" ? "active" : ""}`} aria-pressed={provider === "gitlab"} onClick={() => setProvider("gitlab")} disabled={loading || importing} title={gitlabEnabled ? undefined : t("git.gitlabDisabledTabTitle", "GitLab integration is disabled in Settings")}>GitLab</button>
{gitlabEnabled ? <button type="button" className={`github-import-tab ${provider === "gitlab" ? "active" : ""}`} aria-pressed={provider === "gitlab"} onClick={() => setProvider("gitlab")} disabled={loading || importing}>GitLab</button> : null}
</div>
{provider === "github" ? (
<>

View File

@@ -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(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
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(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
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(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);
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(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} projectId="project-1" />);
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([