Skip to content

feat: confluence integration #3127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 68 additions & 5 deletions backend/src/services/integrationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
GroupsioIntegrationData,
GroupsioVerifyGroup,
} from '@/serverless/integrations/usecases/groupsio/types'
import { ConfluenceIntegrationData } from '@/types/confluenceTypes'

import { DISCORD_CONFIG, GITHUB_CONFIG, GITLAB_CONFIG, IS_TEST_ENV, KUBE_MODE } from '../conf/index'
import GithubReposRepository from '../database/repositories/githubReposRepository'
Expand Down Expand Up @@ -1009,23 +1010,85 @@ export default class IntegrationService {
* @param integrationData to create the integration object
* @returns integration object
*/
async confluenceConnectOrUpdate(integrationData) {
async confluenceConnectOrUpdate(integrationData: ConfluenceIntegrationData) {
const transaction = await SequelizeRepository.createTransaction(this.options)
let integration
let integration: any
let connectionId: string
try {
const constructNangoConnectionPayload = (
integrationData: ConfluenceIntegrationData,
): Record<string, any> => {
let confluenceIntegrationType: NangoIntegration
// nangoPayload is different for each integration
// check https://github.com/NangoHQ/nango/blob/master/packages/providers/providers.yaml#L2547
let nangoPayload: any
const ATLASSIAN_CLOUD_SUFFIX = '.atlassian.net' as const

const baseUrl = integrationData.settings.url.trim()
const hostname = new URL(baseUrl).hostname
const isCloudUrl = hostname.endsWith(ATLASSIAN_CLOUD_SUFFIX)
const subdomain = isCloudUrl ? hostname.split(ATLASSIAN_CLOUD_SUFFIX)[0] : null

if (isCloudUrl) {
confluenceIntegrationType = NangoIntegration.CONFLUENCE_BASIC
nangoPayload = {
params: {
subdomain,
},
credentials: {
username: process.env.ATLASSIAN_AUTH_USERNAME,
password: process.env.ATLASSIAN_AUTH_PASSWORD,
},
}
return { confluenceIntegrationType, nangoPayload }
}
confluenceIntegrationType = NangoIntegration.CONFLUENCE_DATA_CENTER
nangoPayload = {
params: {
baseUrl,
},
credentials: {
// TODO: double check if this works for DC instance, once we have creds
apiKey: process.env.ATLASSIAN_AUTH_PASSWORD,
},
}

return { confluenceIntegrationType, nangoPayload }
}
const { confluenceIntegrationType, nangoPayload } =
constructNangoConnectionPayload(integrationData)
this.options.log.info(
`conflunece integration type determined: ${confluenceIntegrationType}, starting nango connection...`,
)
connectionId = await connectNangoIntegration(confluenceIntegrationType, nangoPayload)
integration = await this.createOrUpdate(
{
id: connectionId,
platform: PlatformType.CONFLUENCE,
settings: integrationData.settings,
settings: {
...integrationData.settings,
nangoIntegrationName: confluenceIntegrationType,
},
status: 'done',
},
transaction,
)

await setNangoMetadata(NangoIntegration.CONFLUENCE_BASIC, connectionId, {
spaceKeysToSync: integrationData.settings.spaces,
})
await startNangoSync(NangoIntegration.CONFLUENCE_BASIC, connectionId)
await SequelizeRepository.commitTransaction(transaction)
} catch (err) {
} catch (error) {
await SequelizeRepository.rollbackTransaction(transaction)
throw err
if (error instanceof TypeError && error.message.includes('Invalid URL')) {
this.options.log.error(`Invalid url: ${integrationData.settings.url}`)
throw new Error400(this.options.language, 'errors.confluence.invalidUrl')
}
if (error && error.message.includes('credentials')) {
throw new Error400(this.options.language, 'errors.confluence.invalidCredentials')
}
throw error
}
return integration
}
Expand Down
7 changes: 7 additions & 0 deletions backend/src/types/confluenceTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface ConfluenceIntegrationData {
settings: {
url: string
spaces: string[]
}
segments?: string[]
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
<template>
<lf-tooltip
placement="top"
content-class="!max-w-76 !p-3 !text-start"
content="These integrations are temporarily disabled. Please contact the CM team for further questions."
<lf-button
type="secondary"
@click="isConfluenceSettingsDrawerVisible = true"
>
<lf-button
:disabled="true"
type="secondary"
@click="isConfluenceSettingsDrawerVisible = true"
>
<lf-icon name="link-simple" />
<slot>Connect</slot>
</lf-button>
</lf-tooltip>
<lf-icon name="link-simple" />
<slot>Connect</slot>
</lf-button>

<lf-confluence-settings-drawer
v-if="isConfluenceSettingsDrawerVisible"
Expand All @@ -27,7 +20,6 @@
import { defineProps, ref } from 'vue';
import LfIcon from '@/ui-kit/icon/Icon.vue';
import LfButton from '@/ui-kit/button/Button.vue';
import LfTooltip from '@/ui-kit/tooltip/Tooltip.vue';
import LfConfluenceSettingsDrawer from '@/config/integrations/confluence/components/confluence-settings-drawer.vue';

const props = defineProps<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,30 @@
spellcheck="false"
placeholder="Enter Organization URL"
/>
<el-input
v-if="form.space"
id="spaceId"
v-model="form.space.id"
class="text-green-500 mt-2"
spellcheck="false"
placeholder="Enter Space ID"
/>
<el-input
v-if="form.space"
id="spaceKey"
v-model="form.space.key"
<app-array-input
v-for="(_, index) of form.spaces"
:id="`spaceKey-${index}`"
:key="index"
v-model="form.spaces[index]"
class="text-green-500 mt-2"
spellcheck="false"
placeholder="Enter Space Key"
/>
<el-input
v-if="form.space"
id="spaceName"
v-model="form.space.name"
class="text-green-500 mt-2"
spellcheck="false"
placeholder="Enter Space Name"
/>
placeholder="Enter Space key"
>
<template #after>
<lf-button
type="primary-link"
size="medium"
class="w-10 h-10"
icon-only
@click="removeSpaceKey(index)"
>
<lf-icon name="trash-can" :size="20" />
</lf-button>
</template>
</app-array-input>

<lf-button type="primary-link" @click="addSpaceKey()">
+ Add Space Key
</lf-button>
</el-form>
</template>

Expand Down Expand Up @@ -79,7 +79,7 @@
</app-drawer>
</template>

<script setup>
<script setup lang="ts">
import useVuelidate from '@vuelidate/core';
import {
computed, onMounted, reactive, ref,
Expand All @@ -88,9 +88,14 @@ import confluence from '@/config/integrations/confluence/config';
import formChangeDetector from '@/shared/form/form-change';
import { mapActions } from '@/shared/vuex/vuex.helpers';
import useProductTracking from '@/shared/modules/monitoring/useProductTracking';
import { EventType, FeatureEventKey } from '@/shared/modules/monitoring/types/event';
import {
EventType,
FeatureEventKey,
} from '@/shared/modules/monitoring/types/event';
import { Platform } from '@/shared/modules/platform/types/Platform';
import LfButton from '@/ui-kit/button/Button.vue';
import AppArrayInput from '@/shared/form/array-input.vue';
import LfIcon from '@/ui-kit/icon/Icon.vue';

const emit = defineEmits(['update:modelValue']);
const props = defineProps({
Expand All @@ -117,15 +122,16 @@ const { trackEvent } = useProductTracking();
const loading = ref(false);
const form = reactive({
url: '',
space: {
id: '',
key: '',
name: '',
},
spaces: [''],
});

const { hasFormChanged, formSnapshot } = formChangeDetector(form);
const $v = useVuelidate({}, form, { $stopPropagation: true });
const $v = useVuelidate({
url: { required: true },
spaces: {
required: (value: string[]) => value.length > 0 && value.every((v) => v.trim() !== ''),
},
}, form, { $stopPropagation: true });

const { doConfluenceConnect } = mapActions('integration');
const isVisible = computed({
Expand All @@ -138,10 +144,23 @@ const isVisible = computed({
});
const logoUrl = confluence.image;

const addSpaceKey = () => {
form.spaces.push('');
};

const removeSpaceKey = (index: number) => {
form.spaces.splice(index, 1);
};

onMounted(() => {
if (props.integration?.settings) {
form.url = props.integration?.settings.url;
form.space = props.integration?.settings.space;
// to handle both single and multiple spaces
if (props.integration?.settings.space) {
form.spaces = [props.integration?.settings.space.key];
} else {
form.spaces = props.integration?.settings.spaces;
}
}
formSnapshot();
});
Expand All @@ -158,15 +177,17 @@ const connect = async () => {
doConfluenceConnect({
settings: {
url: form.url,
space: form.space,
spaces: form.spaces,
},
isUpdate,
segmentId: props.segmentId,
grandparentId: props.grandparentId,
})
.then(() => {
trackEvent({
key: isUpdate ? FeatureEventKey.EDIT_INTEGRATION_SETTINGS : FeatureEventKey.CONNECT_INTEGRATION,
key: isUpdate
? FeatureEventKey.EDIT_INTEGRATION_SETTINGS
: FeatureEventKey.CONNECT_INTEGRATION,
type: EventType.FEATURE,
properties: {
platform: Platform.CONFLUENCE,
Expand All @@ -181,7 +202,7 @@ const connect = async () => {
};
</script>

<script>
<script lang="ts">
export default {
name: 'LfConfluenceSettingsDrawer',
};
Expand Down
15 changes: 1 addition & 14 deletions frontend/src/modules/lf/layout/components/lf-banners.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<template>
<!-- TODO: Remove the || true once the integrations are back up -->
<div
v-if="showBanner || true"
v-if="showBanner"
>
<div class="pt-14">
<!-- Links to {sub-project} integrations page -->
Expand Down Expand Up @@ -127,18 +126,6 @@
</router-link>
</div>
</banner>
<!-- TODO: Remove this banner once Confluence integrations is back up -->
<banner
variant="alert"
>
<div
class="flex flex-wrap items-center justify-center grow text-sm py-2"
>
<span class="font-semibold">Temporary Disruption of Confluence Integration</span>
<span>&nbsp;Confluence integration is currently stopped.
The team is actively working on bringing the integration back and restore full functionality.</span>
</div>
</banner>
</div>
</div>
</template>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/shared/modules/platform/types/Platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export enum Platform {
EMAILS = 'emails',
PHONE_NUMBERS = 'phoneNumbers',
CRUNCHBASE = 'crunchbase',
CONFLUENCE = 'confluence',
}
16 changes: 13 additions & 3 deletions services/apps/cron_service/src/jobs/nangoMonitoring.job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,20 @@ const job: IJobDefinition = {
if (!nangoConnection) {
ctx.log.warn(`${int.platform} integration with id "${int.id}" is not connected to Nango!`)
} else {
let nangoPlatform: NangoIntegration = int.platform as NangoIntegration
if (int.platform == PlatformType.JIRA || int.platform == PlatformType.CONFLUENCE) {
// those integrations are mapped with multiple nango integrations based on auth method
if (!int.settings.nangoIntegrationName) {
// old integrations in db
ctx.log.warn(
`Could not get nangoIntegrationName from integration.settings for integrationId: ${int.id} - should re-connect it`,
)
continue
}
nangoPlatform = int.settings.nangoIntegrationName as NangoIntegration
}
const results = await getNangoConnectionStatus(
int.platform == PlatformType.JIRA
? (int.settings.nangoIntegrationName as NangoIntegration)
: (int.platform as NangoIntegration),
nangoPlatform,
nangoConnection.connection_id,
)

Expand Down
2 changes: 1 addition & 1 deletion services/apps/cron_service/src/jobs/nangoTrigger.job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const job: IJobDefinition = {

const platform = platformToNangoIntegration(int.platform as PlatformType, settings)

if (platform === NangoIntegration.GITHUB && !settings.nangoMapping) {
if (!platform || (platform === NangoIntegration.GITHUB && !settings.nangoMapping)) {
// ignore non-nango github integrations
continue
}
Expand Down
6 changes: 6 additions & 0 deletions services/libs/common/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ const en = {
invalidCredentials:
'The given credentials were found to be invalid. Please check the credentials and try again',
},
confluence: {
invalidUrl:
'The URL provided is invalid. Please enter a valid URL format such as https://example.com or https://example.atlassian.net',
invalidCredentials:
'The given credentials were found to be invalid. Please check the credentials and try again',
},
groupsio: {
isTwoFactorRequired: 'Two-factor authentication code is required',
invalidCredentials: 'Invalid email or password',
Expand Down
3 changes: 3 additions & 0 deletions services/libs/nango/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ export const getNangoConnectionStatus = async (
): Promise<SyncStatus[]> => {
ensureBackendClient()

log.info(
`Fetching nango syncs status for connection ${connectionId} and integration ${integration}`,
)
const res = await backendClient.syncStatus(integration, '*', connectionId)

return res.syncs
Expand Down
Loading
Loading