Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
258e36d
feat(connectors): implement Confluence connector across frontend
daniilperkin Aug 28, 2026
0615611
fix(ingestion): create Confluence data sources from connections list
daniilperkin Aug 28, 2026
8fe5692
feat(ingestion): add toast feedback to Confluence space update
daniilperkin Aug 28, 2026
eeeaf22
fix(ingestion): align Confluence sourceId and status matching with ba…
daniilperkin Aug 28, 2026
1dbf41f
feat(confluence): add schedule API types and update toast dependencies
daniilperkin Aug 28, 2026
9e9e9ba
feat(knowledge-base): support rich markdown rendering for Confluence …
daniilperkin Aug 28, 2026
31166fc
Merge branch 'feature/minor-upgrades' into feature/269-confluence-con…
daniilperkin Aug 28, 2026
b881f3a
Merge branch 'feature/293-KB-gh-org-lvl-data' into feature/269-conflu…
daniilperkin Aug 28, 2026
48705c1
Merge remote-tracking branch 'origin/feature/minor-upgrades' into fea…
daniilperkin Aug 28, 2026
34be14c
Merge remote-tracking branch 'origin/feature/293-KB-gh-org-lvl-data' …
daniilperkin Aug 28, 2026
d0f847f
fix(confluence): address code review findings from PR #185
daniilperkin Aug 29, 2026
c847846
fix(confluence): align ingestion run query filter and source label re…
daniilperkin Aug 29, 2026
387125c
Move Jira credential CRUD into a shared Atlassian credential module
kiranfin Sep 5, 2026
1e767fe
Show the readable space name and credential on Confluence sources
kiranfin Sep 5, 2026
e8601b8
Connect spaces through a stored Atlassian credential
kiranfin Sep 5, 2026
d231dd1
Show the Confluence base URL under the space name on data ingestion s…
kiranfin Sep 6, 2026
0fea450
Merge 'dev' into 'feature/269-confluence-connector'
kiranfin Sep 6, 2026
5a8e68a
Merge branch 'dev' into feature/269-confluence-connector
kiranfin Sep 8, 2026
b6e4664
Align the Confluence connector modal and add remove-from-project
kiranfin Sep 8, 2026
beda68f
Fix three Confluence connector issues
kiranfin Sep 8, 2026
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
20 changes: 10 additions & 10 deletions src/features/access/components/connectorAdapters.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useAuth } from "../../../context/useAuth";
import { TokenRow } from "../../settings/components/TokenRow";
import { JiraCredentialAddForm } from "../../settings/components/jira/JiraCredentialAddForm";
import { JiraCredentialRow } from "../../settings/components/jira/JiraCredentialRow";
import type { JiraCredentialsDto } from "../../../services/sources/jiraService";
import { AtlassianCredentialAddForm } from "../../settings/components/atlassian/AtlassianCredentialAddForm";
import { AtlassianCredentialRow } from "../../settings/components/atlassian/AtlassianCredentialRow";
import type { AtlassianCredentialDto } from "../../../services/sources/atlassianService";
import type { AccessAddFormProps, AccessRowProps } from "../types";

/**
Expand All @@ -18,20 +18,20 @@ export function GithubTokenRow({ entry, onSaved }: AccessRowProps<string>) {
return <TokenRow name={entry.payload} onSaved={onSaved} />;
}

export function JiraAccessRow({ entry, onSaved }: AccessRowProps<JiraCredentialsDto>) {
return <JiraCredentialRow credential={entry.payload} onSaved={onSaved} />;
export function AtlassianAccessRow({ entry, onSaved }: AccessRowProps<AtlassianCredentialDto>) {
return <AtlassianCredentialRow credential={entry.payload} onSaved={onSaved} />;
}

/**
* The Jira form needs a default account email, which every other connector's
* form does not β€” so the profile is read here rather than threaded through the
* view and the registry.
* The Atlassian form needs a default account email, which every other
* connector's form does not β€” so the profile is read here rather than
* threaded through the view and the registry.
*/
export function JiraAccessAddForm({ onClose, onSaved }: AccessAddFormProps) {
export function AtlassianAccessAddForm({ onClose, onSaved }: AccessAddFormProps) {
const { profile } = useAuth();

return (
<JiraCredentialAddForm
<AtlassianCredentialAddForm
defaultUserEmail={profile?.email ?? null}
onClose={onClose}
onSaved={onSaved}
Expand Down
28 changes: 16 additions & 12 deletions src/features/access/registry.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useMemo } from "react";
import { GitBranch, Ticket } from "lucide-react";
import { useGithubTokens } from "../settings/hooks/useGithubTokens";
import { useJiraCredentials } from "../settings/hooks/useJiraCredentials";
import { useAtlassianCredentials } from "../settings/hooks/useAtlassianCredentials";
import { TokenAddForm } from "../settings/components/TokenAddForm";
import { GithubTokenRow, JiraAccessAddForm, JiraAccessRow } from "./components/connectorAdapters";
import type { JiraCredentialsDto } from "../../services/sources/jiraService";
import {
GithubTokenRow,
AtlassianAccessAddForm,
AtlassianAccessRow,
} from "./components/connectorAdapters";
import type { AtlassianCredentialDto } from "../../services/sources/atlassianService";
import type { AccessConnector } from "./types";

/**
Expand Down Expand Up @@ -48,19 +52,19 @@ export const githubConnector = defineAccessConnector<string>({
Row: GithubTokenRow,
});

export const jiraConnector = defineAccessConnector<JiraCredentialsDto>({
id: "jira",
label: "Jira",
export const atlassianConnector = defineAccessConnector<AtlassianCredentialDto>({
id: "atlassian",
label: "Atlassian",
icon: Ticket,
noun: { one: "credential", many: "credentials" },
addLabel: "Add credential",
emptyTitle: "No credentials yet",
emptyDescription: "Add a Jira API token to connect and ingest Jira instances.",
emptyDescription: "Add an Atlassian API token to connect Jira instances and Confluence spaces.",
useEntries: () => {
const { credentials, loaded, error, isRefreshing, reload } = useJiraCredentials();
const { credentials, loaded, error, isRefreshing, reload } = useAtlassianCredentials();

// Credentials are keyed by `(userEmail, tokenName)` server-side; the same
// name may exist for two Jira accounts, so the key needs both.
// name may exist for two Atlassian accounts, so the key needs both.
const entries = useMemo(
() =>
credentials.map((credential) => ({
Expand All @@ -72,8 +76,8 @@ export const jiraConnector = defineAccessConnector<JiraCredentialsDto>({

return { entries, loaded, error, isRefreshing, reload };
},
AddForm: JiraAccessAddForm,
Row: JiraAccessRow,
AddForm: AtlassianAccessAddForm,
Row: AtlassianAccessRow,
});

/**
Expand All @@ -83,4 +87,4 @@ export const jiraConnector = defineAccessConnector<JiraCredentialsDto>({
* form β€” no new tab, no page change, no layout change. Keep the list short
* enough that the source filter stays useful.
*/
export const ACCESS_CONNECTORS: AccessConnector[] = [githubConnector, jiraConnector];
export const ACCESS_CONNECTORS: AccessConnector[] = [githubConnector, atlassianConnector];
85 changes: 76 additions & 9 deletions src/features/admin/components/CreateProjectWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@ import {
import {
addDraftSource,
connectDraftSources,
createConfluenceDraft,
createDraftSourceFromDiscovery,
createJiraDraft,
createUploadDraft,
hasFailedSources,
isValidConfluenceSpaceId,
removeDraftSource,
setDraftSourceOwner,
type DraftSource,
} from "../projectSourcesDraft";
import { sortOwnerOptions } from "../sourceOwners";
import type { DiscoverySelection } from "../../data-ingestion/components/GithubRepositoryDiscovery";
import type { SourceSystem } from "../../data-ingestion/types";
import { useJiraCredentials } from "../../settings/hooks/useJiraCredentials";
import type { JiraCredentialsDto } from "../../../services/sources/jiraService";
import { useAtlassianCredentials } from "../../settings/hooks/useAtlassianCredentials";
import type { AtlassianCredentialDto } from "../../../services/sources/atlassianService";
import { useGithubTokens } from "../../settings/hooks/useGithubTokens";
import { getDisplayName } from "../data";
import type { AdminUser } from "../types";
Expand Down Expand Up @@ -72,8 +74,8 @@ const STEP_INDEX: Record<Exclude<WizardPhase, "provisioning">, number> = {
review: 3,
};

// All three connectors can now be staged from the add-source sub-flow.
const AVAILABLE_SOURCE_TYPES: SourceSystem[] = ["GITHUB", "JIRA", "UPLOAD"];
// All four connectors can now be staged from the add-source sub-flow.
const AVAILABLE_SOURCE_TYPES: SourceSystem[] = ["GITHUB", "JIRA", "UPLOAD", "CONFLUENCE"];

/**
* Transactional create-project wizard: everything is drafted locally across the
Expand Down Expand Up @@ -141,6 +143,10 @@ export function CreateProjectWizard({
const [jiraUrl, setJiraUrl] = useState("");
const [jiraCredentialName, setJiraCredentialName] = useState("");

const [confluenceBaseUrl, setConfluenceBaseUrl] = useState("");
const [confluenceSpaceId, setConfluenceSpaceId] = useState("");
const [confluenceCredentialName, setConfluenceCredentialName] = useState("");

// Upload files staged in memory; uploaded during provisioning once a project
// id exists.
const [uploadFiles, setUploadFiles] = useState<File[]>([]);
Expand Down Expand Up @@ -181,14 +187,17 @@ export function CreateProjectWizard({
};

const isJiraDetail = isAddingSource && addStep === "detail" && addType === "JIRA";
const isConfluenceDetail = isAddingSource && addStep === "detail" && addType === "CONFLUENCE";
// Jira and Confluence share the same Atlassian credential store, so one
// instance of the hook backs both detail screens' pickers.
const {
credentials: jiraCredentials,
loaded: jiraCredentialsLoaded,
error: jiraCredentialsError,
isRefreshing: jiraCredentialsLoading,
reload: reloadJiraCredentials,
addCredentialLocally,
} = useJiraCredentials(isOpen && isJiraDetail);
} = useAtlassianCredentials(isOpen && (isJiraDetail || isConfluenceDetail));

// The token list arrives asynchronously; adopt the first token as soon as it
// does (and heal a stale selection) so discovery is usable on the first open.
Expand All @@ -202,8 +211,10 @@ export function CreateProjectWizard({
});
}, [effectiveTokenNames]);

// Same adoption pattern for the Jira credential picker: select the first
// stored credential once the list arrives, keeping a still-valid choice.
// Same adoption pattern for the Jira and Confluence credential pickers:
// select the first stored credential once the list arrives, keeping a
// still-valid choice. Both fields share the list, so a credential just added
// from either detail screen is adopted here too.
useEffect(() => {
if (!jiraCredentialsLoaded || jiraCredentialsLoading) return;

Expand All @@ -214,6 +225,12 @@ export function CreateProjectWizard({
? current
: jiraCredentials[0].displayName;
});
setConfluenceCredentialName((current) => {
if (jiraCredentials.length === 0) return "";
return current && jiraCredentials.some((credential) => credential.displayName === current)
? current
: jiraCredentials[0].displayName;
});
});
}, [jiraCredentials, jiraCredentialsLoaded, jiraCredentialsLoading]);

Expand All @@ -240,6 +257,12 @@ export function CreateProjectWizard({
void Promise.resolve().then(loadManagerCandidates);
}, [isOpen, loadManagerCandidates]);

const resetConfluenceDraftFields = () => {
setConfluenceBaseUrl("");
setConfluenceSpaceId("");
setConfluenceCredentialName("");
};

const resetWizard = () => {
setPhase("details");
setName("");
Expand All @@ -255,6 +278,7 @@ export function CreateProjectWizard({
setAddType("GITHUB");
setGithubSelection([]);
resetJiraDraftFields();
resetConfluenceDraftFields();
setUploadFiles([]);
setCreatedProjectId("");
};
Expand All @@ -268,6 +292,7 @@ export function CreateProjectWizard({
const resetSourceDraftFields = () => {
setGithubSelection([]);
resetJiraDraftFields();
resetConfluenceDraftFields();
setUploadFiles([]);
};

Expand Down Expand Up @@ -397,24 +422,40 @@ export function CreateProjectWizard({
await loadTokenNames();
};

const handleCredentialSaved = async (credential: JiraCredentialsDto) => {
const handleCredentialSaved = async (credential: AtlassianCredentialDto) => {
addCredentialLocally(credential);
setJiraCredentialName(credential.displayName);
await reloadJiraCredentials();
};

const handleConfluenceCredentialSaved = async (credential: AtlassianCredentialDto) => {
addCredentialLocally(credential);
setConfluenceCredentialName(credential.displayName);
await reloadJiraCredentials();
};

const selectedJiraCredential = jiraCredentials.find(
(credential) => credential.displayName === jiraCredentialName,
);

const selectedConfluenceCredential = jiraCredentials.find(
(credential) => credential.displayName === confluenceCredentialName,
);

const canAddSource =
addType === "GITHUB"
? githubSelection.length > 0
: addType === "JIRA"
? Boolean(jiraDisplayName.trim() && jiraUrl.trim() && selectedJiraCredential)
: addType === "UPLOAD"
? uploadFiles.length > 0
: false;
: addType === "CONFLUENCE"
? Boolean(
confluenceBaseUrl.trim() &&
isValidConfluenceSpaceId(confluenceSpaceId) &&
selectedConfluenceCredential,
)
: false;

const commitAddSource = () => {
if (!canAddSource) return;
Expand Down Expand Up @@ -442,6 +483,17 @@ export function CreateProjectWizard({
} else if (addType === "UPLOAD") {
const displayName = uploadFiles.length === 1 ? uploadFiles[0].name : "Uploaded documents";
setSources((current) => addDraftSource(current, createUploadDraft(displayName, uploadFiles)));
} else if (addType === "CONFLUENCE" && selectedConfluenceCredential) {
setSources((current) =>
addDraftSource(
current,
createConfluenceDraft({
baseUrl: confluenceBaseUrl.trim(),
spaceId: confluenceSpaceId.trim(),
credentialName: selectedConfluenceCredential.displayName,
}),
),
);
}

closeAddSource();
Expand Down Expand Up @@ -804,6 +856,21 @@ export function CreateProjectWizard({
current.filter((_, position) => position !== index),
),
}}
confluence={{
baseUrl: confluenceBaseUrl,
spaceId: confluenceSpaceId,
credentialName: confluenceCredentialName,
credentials: jiraCredentials,
credentialsLoaded: jiraCredentialsLoaded,
credentialsLoading: jiraCredentialsLoading,
credentialsError: jiraCredentialsError,
defaultUserEmail: null,
onBaseUrlChange: setConfluenceBaseUrl,
onSpaceIdChange: setConfluenceSpaceId,
onCredentialNameChange: setConfluenceCredentialName,
onSubmit: commitAddSource,
onCredentialSaved: handleConfluenceCredentialSaved,
}}
/>
) : (
<WizardSourcesStep
Expand Down
9 changes: 9 additions & 0 deletions src/features/admin/components/StagedSourceList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AlertCircle,
BookOpen,
Check,
FileText,
GitBranch,
Expand Down Expand Up @@ -55,6 +56,10 @@ function TypeIcon({ source }: { source: DraftSource }) {
return <FileText className="h-4 w-4 text-app-text-muted" />;
}

if (source.type === "CONFLUENCE") {
return <BookOpen className="h-4 w-4 text-app-text-muted" />;
}

return <GitBranch className="h-4 w-4 text-app-text-muted" />;
}

Expand Down Expand Up @@ -95,6 +100,10 @@ function sourceDetail(source: DraftSource): string {
return source.url;
}

if (source.type === "CONFLUENCE") {
return `${source.baseUrl} (${source.spaceId})`;
}

return source.tokenName;
}

Expand Down
Loading
Loading