From d63d2293277dd89556787b6e0f9de857137f46e0 Mon Sep 17 00:00:00 2001 From: "Asaph M. Kotzin" Date: Fri, 3 Apr 2026 14:43:20 +0100 Subject: [PATCH 1/2] feat(gitlab): add frontend components for GitLab integration Add RTK Query services, setup page, resource selector, and integration list support for the GitLab integration. Setup page split into three components per review feedback. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/common/constants.ts | 16 ++ .../common/hooks/useHasGitlabIntegration.ts | 12 ++ frontend/common/services/useGitlab.ts | 73 ++++++++ .../common/services/useGitlabIntegration.ts | 118 ++++++++++++ frontend/common/stores/default-flags.ts | 26 +++ frontend/common/types/requests.ts | 30 +++ frontend/common/types/responses.ts | 21 +++ frontend/common/utils/utils.tsx | 4 +- .../components/ExternalResourcesLinkTab.tsx | 125 +++++++++++-- .../web/components/ExternalResourcesTable.tsx | 14 +- .../web/components/GitLabResourcesSelect.tsx | 152 +++++++++++++++ frontend/web/components/IntegrationList.tsx | 138 ++++++++------ .../modals/CreateEditIntegrationModal.tsx | 14 +- .../modals/create-feature/index.tsx | 40 ++-- .../web/components/mv/VariationValueInput.tsx | 6 +- .../pages/CreateGitLabIntegrationForm.tsx | 80 ++++++++ .../pages/GitLabIntegrationDetails.tsx | 173 ++++++++++++++++++ .../web/components/pages/GitLabSetupPage.tsx | 62 +++++++ 18 files changed, 1003 insertions(+), 101 deletions(-) create mode 100644 frontend/common/hooks/useHasGitlabIntegration.ts create mode 100644 frontend/common/services/useGitlab.ts create mode 100644 frontend/common/services/useGitlabIntegration.ts create mode 100644 frontend/web/components/GitLabResourcesSelect.tsx create mode 100644 frontend/web/components/pages/CreateGitLabIntegrationForm.tsx create mode 100644 frontend/web/components/pages/GitLabIntegrationDetails.tsx create mode 100644 frontend/web/components/pages/GitLabSetupPage.tsx diff --git a/frontend/common/constants.ts b/frontend/common/constants.ts index df68667a4f6b..acca3d6df8ee 100644 --- a/frontend/common/constants.ts +++ b/frontend/common/constants.ts @@ -492,6 +492,10 @@ const Constants = { githubIssue: 'GitHub Issue', githubPR: 'Github PR', }, + gitlabType: { + gitlabIssue: 'GitLab Issue', + gitlabMR: 'GitLab MR', + }, integrationCategoryDescriptions: { 'All': 'Send data on what flags served to each identity.', 'Analytics': 'Send data on what flags served to each identity.', @@ -685,6 +689,18 @@ const Constants = { resourceType: 'pulls', type: 'GITHUB', }, + GITLAB_ISSUE: { + id: 3, + label: 'Issue', + resourceType: 'issues', + type: 'GITLAB', + }, + GITLAB_MR: { + id: 4, + label: 'Merge Request', + resourceType: 'merge_requests', + type: 'GITLAB', + }, }, roles: { 'ADMIN': 'Organisation Administrator', diff --git a/frontend/common/hooks/useHasGitlabIntegration.ts b/frontend/common/hooks/useHasGitlabIntegration.ts new file mode 100644 index 000000000000..fb961cd5bf91 --- /dev/null +++ b/frontend/common/hooks/useHasGitlabIntegration.ts @@ -0,0 +1,12 @@ +import { useGetGitlabIntegrationQuery } from 'common/services/useGitlabIntegration' + +export function useHasGitlabIntegration(projectId: number) { + const { data } = useGetGitlabIntegrationQuery( + { project_id: projectId }, + { skip: !projectId }, + ) + + return { + hasIntegration: !!data?.results?.length, + } +} diff --git a/frontend/common/services/useGitlab.ts b/frontend/common/services/useGitlab.ts new file mode 100644 index 000000000000..219618674001 --- /dev/null +++ b/frontend/common/services/useGitlab.ts @@ -0,0 +1,73 @@ +import { Res } from 'common/types/responses' +import { Req } from 'common/types/requests' +import { service } from 'common/service' +import Utils from 'common/utils/utils' + +export const gitlabService = service + .enhanceEndpoints({ addTagTypes: ['Gitlab'] }) + .injectEndpoints({ + endpoints: (builder) => ({ + getGitlabProjects: builder.query< + Res['gitlabProjects'], + Req['getGitlabProjects'] + >({ + providesTags: [{ id: 'LIST', type: 'Gitlab' }], + query: (query: Req['getGitlabProjects']) => ({ + url: `projects/${query.project_id}/gitlab/projects/`, + }), + }), + getGitlabResources: builder.query< + Res['gitlabResources'], + Req['getGitlabResources'] + >({ + providesTags: [{ id: 'LIST', type: 'Gitlab' }], + query: (query: Req['getGitlabResources']) => ({ + url: + `projects/${query.project_id}/gitlab/${query.gitlab_resource}/` + + `?${Utils.toParam({ + gitlab_project_id: query.gitlab_project_id, + page: query.page, + page_size: query.page_size, + project_name: query.project_name, + })}`, + }), + }), + // END OF ENDPOINTS + }), + }) + +export async function getGitlabResources( + store: any, + data: Req['getGitlabResources'], + options?: Parameters< + typeof gitlabService.endpoints.getGitlabResources.initiate + >[1], +) { + return store.dispatch( + gitlabService.endpoints.getGitlabResources.initiate(data, options), + ) +} +export async function getGitlabProjects( + store: any, + data: Req['getGitlabProjects'], + options?: Parameters< + typeof gitlabService.endpoints.getGitlabProjects.initiate + >[1], +) { + return store.dispatch( + gitlabService.endpoints.getGitlabProjects.initiate(data, options), + ) +} +// END OF FUNCTION_EXPORTS + +export const { + useGetGitlabProjectsQuery, + useGetGitlabResourcesQuery, + // END OF EXPORTS +} = gitlabService + +/* Usage examples: +const { data, isLoading } = useGetGitlabResourcesQuery({ project_id: 2, gitlab_resource: 'issues', gitlab_project_id: 1, project_name: 'my-project' }, {}) //get hook +const { data, isLoading } = useGetGitlabProjectsQuery({ project_id: 2 }, {}) //get hook +gitlabService.endpoints.getGitlabProjects.select({ project_id: 2 })(store.getState()) //access data from any function +*/ diff --git a/frontend/common/services/useGitlabIntegration.ts b/frontend/common/services/useGitlabIntegration.ts new file mode 100644 index 000000000000..eb98a32ac4bf --- /dev/null +++ b/frontend/common/services/useGitlabIntegration.ts @@ -0,0 +1,118 @@ +import { Res } from 'common/types/responses' +import { Req } from 'common/types/requests' +import { service } from 'common/service' + +export const gitlabIntegrationService = service + .enhanceEndpoints({ addTagTypes: ['GitlabIntegration'] }) + .injectEndpoints({ + endpoints: (builder) => ({ + createGitlabIntegration: builder.mutation< + Res['gitlabIntegrations'], + Req['createGitlabIntegration'] + >({ + invalidatesTags: [{ id: 'LIST', type: 'GitlabIntegration' }], + query: (query: Req['createGitlabIntegration']) => ({ + body: query.body, + method: 'POST', + url: `projects/${query.project_id}/integrations/gitlab/`, + }), + }), + deleteGitlabIntegration: builder.mutation< + Res['gitlabIntegrations'], + Req['deleteGitlabIntegration'] + >({ + invalidatesTags: [{ id: 'LIST', type: 'GitlabIntegration' }], + query: (query: Req['deleteGitlabIntegration']) => ({ + method: 'DELETE', + url: `projects/${query.project_id}/integrations/gitlab/${query.gitlab_integration_id}/`, + }), + }), + getGitlabIntegration: builder.query< + Res['gitlabIntegrations'], + Req['getGitlabIntegration'] + >({ + providesTags: [{ id: 'LIST', type: 'GitlabIntegration' }], + query: (query: Req['getGitlabIntegration']) => ({ + url: `projects/${query.project_id}/integrations/gitlab/`, + }), + }), + updateGitlabIntegration: builder.mutation< + Res['gitlabIntegrations'], + Req['updateGitlabIntegration'] + >({ + invalidatesTags: [{ id: 'LIST', type: 'GitlabIntegration' }], + query: (query: Req['updateGitlabIntegration']) => ({ + body: query.body, + method: 'PATCH', + url: `projects/${query.project_id}/integrations/gitlab/${query.gitlab_integration_id}/`, + }), + }), + // END OF ENDPOINTS + }), + }) + +export async function createGitlabIntegration( + store: any, + data: Req['createGitlabIntegration'], + options?: Parameters< + typeof gitlabIntegrationService.endpoints.createGitlabIntegration.initiate + >[1], +) { + return store.dispatch( + gitlabIntegrationService.endpoints.createGitlabIntegration.initiate( + data, + options, + ), + ) +} +export async function deleteGitlabIntegration( + store: any, + data: Req['deleteGitlabIntegration'], + options?: Parameters< + typeof gitlabIntegrationService.endpoints.deleteGitlabIntegration.initiate + >[1], +) { + return store.dispatch( + gitlabIntegrationService.endpoints.deleteGitlabIntegration.initiate( + data, + options, + ), + ) +} +export async function getGitlabIntegration( + store: any, + data: Req['getGitlabIntegration'], + options?: Parameters< + typeof gitlabIntegrationService.endpoints.getGitlabIntegration.initiate + >[1], +) { + return store.dispatch( + gitlabIntegrationService.endpoints.getGitlabIntegration.initiate( + data, + options, + ), + ) +} +export async function updateGitlabIntegration( + store: any, + data: Req['updateGitlabIntegration'], + options?: Parameters< + typeof gitlabIntegrationService.endpoints.updateGitlabIntegration.initiate + >[1], +) { + return store.dispatch( + gitlabIntegrationService.endpoints.updateGitlabIntegration.initiate( + data, + options, + ), + ) +} +// END OF FUNCTION_EXPORTS + +export const { + useCreateGitlabIntegrationMutation, + useDeleteGitlabIntegrationMutation, + useGetGitlabIntegrationQuery, + useUpdateGitlabIntegrationMutation, + // END OF EXPORTS +} = gitlabIntegrationService diff --git a/frontend/common/stores/default-flags.ts b/frontend/common/stores/default-flags.ts index 7bd8d4415353..8db4e8e48100 100644 --- a/frontend/common/stores/default-flags.ts +++ b/frontend/common/stores/default-flags.ts @@ -87,6 +87,32 @@ const defaultFlags = { 'tags': ['logging'], 'title': 'Dynatrace', }, + 'gitlab': { + 'description': + 'View your Flagsmith flags inside GitLab issues and merge requests.', + 'docs': + 'https://docs.flagsmith.com/integrations/project-management/gitlab', + 'fields': [ + { + 'default': 'https://gitlab.com', + 'key': 'gitlab_instance_url', + 'label': 'GitLab Instance URL', + }, + { + 'hidden': true, + 'key': 'access_token', + 'label': 'Access Token', + }, + { + 'key': 'webhook_secret', + 'label': 'Webhook Secret', + }, + ], + 'image': '/static/images/integrations/gitlab.svg', + 'isGitlabIntegration': true, + 'perEnvironment': false, + 'title': 'GitLab', + }, 'grafana': { 'description': 'Receive Flagsmith annotations to your Grafana instance on feature flag and segment changes.', diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index fc48e4e17841..3abb9fdb7bce 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -623,6 +623,36 @@ export type Req = { github_resource: string }> getGithubRepos: { installation_id: string; organisation_id: number } + // GitLab + getGitlabIntegration: { project_id: number; id?: number } + createGitlabIntegration: { + project_id: number + body: { + gitlab_instance_url: string + access_token: string + webhook_secret: string + } + } + updateGitlabIntegration: { + project_id: number + gitlab_integration_id: number + body: { + gitlab_project_id?: number + project_name?: string + tagging_enabled?: boolean + } + } + deleteGitlabIntegration: { + project_id: number + gitlab_integration_id: number + } + getGitlabResources: PagedRequest<{ + project_id: number + gitlab_project_id: number + project_name: string + gitlab_resource: string + }> + getGitlabProjects: { project_id: number } getServersideEnvironmentKeys: { environmentId: string } deleteServersideEnvironmentKeys: { environmentId: string; id: string } createServersideEnvironmentKeys: { diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 2265891e198c..5e8e2c85d33e 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -321,6 +321,7 @@ export type IntegrationData = { image: string fields: IntegrationField[] | undefined isExternalInstallation: boolean + isGitlabIntegration?: boolean perEnvironment: boolean title?: string organisation?: string @@ -1162,6 +1163,26 @@ export type Res = { githubRepository: PagedResponse githubResources: GitHubPagedResponse githubRepos: GithubPaginatedRepos + // GitLab + gitlabIntegration: { + id: number + gitlab_instance_url: string + webhook_secret: string + project: number + } + gitlabIntegrations: PagedResponse + GitlabResource: { + web_url: string + id: number + iid: number + title: string + state: string + merged: boolean + draft: boolean + } + gitlabResources: PagedResponse + GitlabProject: { id: number; name: string; path_with_namespace: string } + gitlabProjects: PagedResponse segmentPriorities: {} featureSegment: FeatureState['feature_segment'] featureVersions: PagedResponse diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index 88389946f1a4..65098a70af0d 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -377,10 +377,12 @@ const Utils = Object.assign({}, require('./base/_utils'), { return 'identities' }, getIntegrationData() { - return Utils.getFlagsmithJSONValue( + const data = Utils.getFlagsmithJSONValue( 'integration_data', defaultFlags.integration_data, ) + // Merge default integration entries that may not be in the remote flag yet + return { ...defaultFlags.integration_data, ...data } }, getIsEdge() { const model = ProjectStore.model as null | ProjectType diff --git a/frontend/web/components/ExternalResourcesLinkTab.tsx b/frontend/web/components/ExternalResourcesLinkTab.tsx index f3c34443e4d3..9c0dc1078674 100644 --- a/frontend/web/components/ExternalResourcesLinkTab.tsx +++ b/frontend/web/components/ExternalResourcesLinkTab.tsx @@ -1,44 +1,55 @@ import React, { FC, useState } from 'react' -import ExternalResourcesTable, { - ExternalResourcesTableBase, -} from './ExternalResourcesTable' -import { ExternalResource, GithubResource } from 'common/types/responses' +import ExternalResourcesTable from './ExternalResourcesTable' +import { ExternalResource, GithubResource, Res } from 'common/types/responses' import { useCreateExternalResourceMutation } from 'common/services/useExternalResource' import Constants from 'common/constants' import GitHubResourcesSelect from './GitHubResourcesSelect' +import GitLabResourcesSelect from './GitLabResourcesSelect' import AppActions from 'common/dispatcher/app-actions' +type VcsProvider = 'github' | 'gitlab' + type ExternalResourcesLinkTabType = { githubId: string + hasIntegrationWithGitlab: boolean organisationId: number featureId: string projectId: number environmentId: string } -type AddExternalResourceRowType = ExternalResourcesTableBase & { - selectedResources?: ExternalResource[] - environmentId: string - githubId: string -} - const ExternalResourcesLinkTab: FC = ({ environmentId, featureId, githubId, + hasIntegrationWithGitlab, organisationId, projectId, }) => { const githubTypes = Object.values(Constants.resourceTypes).filter( (v) => v.type === 'GITHUB', ) + const gitlabTypes = Object.values(Constants.resourceTypes).filter( + (v) => v.type === 'GITLAB', + ) + + const hasGithub = !!githubId + const hasGitlab = hasIntegrationWithGitlab + + const defaultProvider: VcsProvider = + hasGitlab && !hasGithub ? 'gitlab' : 'github' + const defaultResourceType = + defaultProvider === 'gitlab' + ? gitlabTypes[0]?.resourceType + : githubTypes[0]?.resourceType + const [vcsProvider, setVcsProvider] = useState(defaultProvider) const [createExternalResource] = useCreateExternalResourceMutation() - const [resourceType, setResourceType] = useState(githubTypes[0].resourceType) + const [resourceType, setResourceType] = useState(defaultResourceType) const [selectedResources, setSelectedResources] = useState() - const addResource = (featureExternalResource: GithubResource) => { + const addGithubResource = (featureExternalResource: GithubResource) => { const type = Object.keys(Constants.resourceTypes).find( (key: string) => Constants.resourceTypes[key as keyof typeof Constants.resourceTypes] @@ -63,16 +74,90 @@ const ExternalResourcesLinkTab: FC = ({ AppActions.refreshFeatures(projectId, environmentId) }) } + + const addGitlabResource = ( + featureExternalResource: Res['GitlabResource'], + ) => { + const type = Object.keys(Constants.resourceTypes).find((key: string) => { + const rt = + Constants.resourceTypes[key as keyof typeof Constants.resourceTypes] + return rt.resourceType === resourceType && rt.type === 'GITLAB' + }) + createExternalResource({ + body: { + feature: parseInt(featureId), + metadata: { + 'draft': featureExternalResource.draft, + 'merged': featureExternalResource.merged, + 'state': featureExternalResource.state, + 'title': featureExternalResource.title, + }, + type: type, + url: featureExternalResource.web_url, + }, + feature_id: featureId, + project_id: projectId, + }).then((res) => { + if ('error' in res) { + toast(`Error adding resource: ${JSON.stringify(res.error)}`, 'danger') + } else { + toast('External Resource Added') + } + AppActions.refreshFeatures(projectId, environmentId) + }) + } + + const handleProviderChange = (provider: VcsProvider) => { + setVcsProvider(provider) + if (provider === 'gitlab') { + setResourceType(gitlabTypes[0]?.resourceType) + } else { + setResourceType(githubTypes[0]?.resourceType) + } + } + return ( <> - v.url!)} - orgId={organisationId} - /> + {hasGithub && hasGitlab && ( +
+ + +
+ )} + {vcsProvider === 'gitlab' && hasGitlab ? ( + v.url ?? '')} + projectId={`${projectId}`} + linkedExternalResources={selectedResources} + /> + ) : ( + v.url ?? '')} + orgId={organisationId as unknown as string} + linkedExternalResources={selectedResources} + /> + )} { + const isGitlab = type?.startsWith('GITLAB') + const match = url.match(/\/(\d+)\/?$/) + const num = match ? match[1] : url.replace(/\D/g, '') + return isGitlab && type === 'GITLAB_MR' ? `!${num}` : `#${num}` +} + export type ExternalResourcesTableBase = { featureId: string projectId: string @@ -61,9 +68,10 @@ const ExternalResourceRow: FC = ({ - {`${ - externalResource?.metadata?.title - } (#${externalResource?.url.replace(/\D/g, '')})`}{' '} + {`${externalResource?.metadata?.title} (${getResourceNumber( + externalResource?.url, + externalResource?.type, + )})`}{' '}
diff --git a/frontend/web/components/GitLabResourcesSelect.tsx b/frontend/web/components/GitLabResourcesSelect.tsx new file mode 100644 index 000000000000..6cd3571c7532 --- /dev/null +++ b/frontend/web/components/GitLabResourcesSelect.tsx @@ -0,0 +1,152 @@ +import React, { FC, useState } from 'react' +import { ExternalResource, Res } from 'common/types/responses' +import Utils from 'common/utils/utils' +import useInfiniteScroll from 'common/useInfiniteScroll' +import { Req } from 'common/types/requests' +import { + useGetGitlabProjectsQuery, + useGetGitlabResourcesQuery, +} from 'common/services/useGitlab' +import Constants from 'common/constants' + +export type GitLabResourcesSelectType = { + onChange: (value: string) => void + linkedExternalResources: ExternalResource[] | undefined + projectId: string + resourceType: string + value: string[] | undefined // an array of resource URLs + setResourceType: (value: string) => void +} + +type GitLabResourcesValueType = { + value: string +} + +const GitLabResourcesSelect: FC = ({ + onChange, + projectId, + resourceType, + setResourceType, + value, +}) => { + const gitlabTypes = Object.values(Constants.resourceTypes).filter( + (v) => v.type === 'GITLAB', + ) + const [selectedProject, setSelectedProject] = useState('') + const gitlabProjectId = selectedProject + ? parseInt(selectedProject.split('::')[0]) + : undefined + const projectName = selectedProject + ? selectedProject.split('::')[1] + : undefined + + const { data: gitlabProjects } = useGetGitlabProjectsQuery({ + project_id: parseInt(projectId), + }) + + const { data, isFetching, isLoading, searchItems } = useInfiniteScroll< + Req['getGitlabResources'], + Res['gitlabResources'] + >( + useGetGitlabResourcesQuery, + { + gitlab_project_id: gitlabProjectId || 0, + gitlab_resource: resourceType, + page_size: 100, + project_id: parseInt(projectId), + project_name: projectName || '', + }, + 100, + { skip: !resourceType || !projectId || !gitlabProjectId || !projectName }, + ) + + const [searchText, setSearchText] = React.useState('') + + return ( + <> + +
+
+ v.resourceType === resourceType)} + onChange={(v: { resourceType: string }) => + setResourceType(v.resourceType) + } + options={gitlabTypes.map((e) => { + return { + label: e.label, + resourceType: e.resourceType, + value: e.id, + } + })} + /> +
+
+ {!!gitlabProjectId && !!projectName && ( +
+ { + const found = gitlabProjects?.results?.find((p) => p.id === v.value) + setSelectedGitlabProject(found || null) + }} + options={gitlabProjects?.results?.map((p) => ({ label: p.path_with_namespace, value: p.id }))} + /> + {selectedGitlabProject && ( + + )} +
+ )} + + + {gitlabIntegration.project_name && ( +
+ + { + onUpdate({ + body: { tagging_enabled: !gitlabIntegration.tagging_enabled }, + gitlab_integration_id: gitlabIntegration.id, + project_id: parseInt(projectId), + }) + }} + /> + Enable automatic tagging of features based on issue/MR state + +
+ )} + +
+ +

+ Add this webhook URL to your GitLab project or group settings. Enable triggers for: Issues events, Merge request events. +

+ + + {webhookUrl} + + + + + + {showSecret ? gitlabIntegration.webhook_secret : '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022'} + + + + +
+ +
+ +
+ + ) +} + +export default GitLabIntegrationDetails diff --git a/frontend/web/components/pages/GitLabSetupPage.tsx b/frontend/web/components/pages/GitLabSetupPage.tsx new file mode 100644 index 000000000000..a322bf2249dd --- /dev/null +++ b/frontend/web/components/pages/GitLabSetupPage.tsx @@ -0,0 +1,62 @@ +import React, { FC } from 'react' +import { + useCreateGitlabIntegrationMutation, + useDeleteGitlabIntegrationMutation, + useGetGitlabIntegrationQuery, + useUpdateGitlabIntegrationMutation, +} from 'common/services/useGitlabIntegration' +import { useGetGitlabProjectsQuery } from 'common/services/useGitlab' +import Project from 'common/project' +import GitLabIntegrationDetails from './GitLabIntegrationDetails' +import CreateGitLabIntegrationForm from './CreateGitLabIntegrationForm' + +type GitLabSetupPageType = { + projectId: string +} + +const GitLabSetupPage: FC = ({ projectId }) => { + const [createGitlabIntegration, { isLoading: isCreating }] = + useCreateGitlabIntegrationMutation() + const [updateGitlabIntegration] = useUpdateGitlabIntegrationMutation() + const [deleteGitlabIntegration] = useDeleteGitlabIntegrationMutation() + + const { data: gitlabIntegrations } = useGetGitlabIntegrationQuery( + { project_id: parseInt(projectId) }, + { skip: !projectId }, + ) + + const gitlabIntegration = gitlabIntegrations?.results?.[0] + + const { data: gitlabProjects } = useGetGitlabProjectsQuery( + { project_id: parseInt(projectId) }, + { skip: !gitlabIntegration }, + ) + + const apiHost = Project.api + ? Project.api.replace(/\/api\/v1\/?$/, '') + : window.location.origin + const webhookUrl = `${apiHost}/api/v1/gitlab-webhook/${projectId}/` + + if (gitlabIntegration) { + return ( + + ) + } + + return ( + + ) +} + +export default GitLabSetupPage From e10af1d1977a238346c5ce0fa3c2e02cdf00ff38 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:23:21 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- api/integrations/github/github.py | 9 --------- api/integrations/gitlab/mappers.py | 4 +--- api/integrations/gitlab/services.py | 4 +++- api/integrations/gitlab/tasks.py | 8 ++++---- .../integrations/gitlab/test_unit_gitlab_services.py | 10 ++++++++-- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/api/integrations/github/github.py b/api/integrations/github/github.py index 1c9f2f9696de..f6cbcda0e272 100644 --- a/api/integrations/github/github.py +++ b/api/integrations/github/github.py @@ -6,19 +6,10 @@ from django.db.models import Q from django.utils.formats import get_format -from core.helpers import get_current_site_url from features.models import Feature, FeatureState, FeatureStateValue from integrations.github.constants import ( - DELETED_FEATURE_TEXT, - DELETED_SEGMENT_OVERRIDE_TEXT, - FEATURE_ENVIRONMENT_URL, - FEATURE_TABLE_HEADER, - FEATURE_TABLE_ROW, GITHUB_TAG_COLOR, - LINK_FEATURE_TITLE, - LINK_SEGMENT_TITLE, UNLINKED_FEATURE_TEXT, - UPDATED_FEATURE_TEXT, GitHubEventType, GitHubTag, github_tag_description, diff --git a/api/integrations/gitlab/mappers.py b/api/integrations/gitlab/mappers.py index a9d9c147f9b3..e6cf08e172e0 100644 --- a/api/integrations/gitlab/mappers.py +++ b/api/integrations/gitlab/mappers.py @@ -29,9 +29,7 @@ def map_feature_states_to_dicts( env_data["last_updated"] = feature_state.updated_at.strftime( get_format("DATETIME_INPUT_FORMATS")[0] ) - env_data["environment_api_key"] = ( - feature_state.environment.api_key # type: ignore[union-attr] - ) + env_data["environment_api_key"] = feature_state.environment.api_key # type: ignore[union-attr] if ( hasattr(feature_state, "feature_segment") diff --git a/api/integrations/gitlab/services.py b/api/integrations/gitlab/services.py index 7ea578f380fd..9b50a1d3d750 100644 --- a/api/integrations/gitlab/services.py +++ b/api/integrations/gitlab/services.py @@ -153,7 +153,9 @@ def dispatch_gitlab_comment( "feature_name": feature.name, "event_type": event_type, "feature_states": feature_states_data, - "url": url if event_type == GitLabEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value else None, + "url": url + if event_type == GitLabEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value + else None, "segment_name": segment_name, }, ) diff --git a/api/integrations/gitlab/tasks.py b/api/integrations/gitlab/tasks.py index ae490e7df601..ebbb845cb4fd 100644 --- a/api/integrations/gitlab/tasks.py +++ b/api/integrations/gitlab/tasks.py @@ -14,7 +14,9 @@ UNLINKED_FEATURE_TEXT = "**The feature flag `%s` was unlinked from the issue/MR**" -def _parse_resource_url(resource_url: str) -> tuple[str, GitLabResourceEndpoint, int] | None: +def _parse_resource_url( + resource_url: str, +) -> tuple[str, GitLabResourceEndpoint, int] | None: """Parse a GitLab resource URL into (project_path, resource_type, iid). Returns None if the URL format is not recognised. @@ -69,9 +71,7 @@ def post_gitlab_comment( deleted_at__isnull=True, ) except GitLabConfiguration.DoesNotExist: - logger.warning( - "No GitLabConfiguration found for project_id=%s", project_id - ) + logger.warning("No GitLabConfiguration found for project_id=%s", project_id) return if not gitlab_config.gitlab_project_id: diff --git a/api/tests/unit/integrations/gitlab/test_unit_gitlab_services.py b/api/tests/unit/integrations/gitlab/test_unit_gitlab_services.py index 72114c7601c3..6057995c2165 100644 --- a/api/tests/unit/integrations/gitlab/test_unit_gitlab_services.py +++ b/api/tests/unit/integrations/gitlab/test_unit_gitlab_services.py @@ -99,7 +99,10 @@ def test_gitlab_webhook__invalid_token__returns_400( url, data="{}", content_type="application/json", - **{"HTTP_X_GITLAB_TOKEN": "wrong-secret", "HTTP_X_GITLAB_EVENT": "Merge Request Hook"}, # type: ignore[arg-type] + **{ + "HTTP_X_GITLAB_TOKEN": "wrong-secret", + "HTTP_X_GITLAB_EVENT": "Merge Request Hook", + }, # type: ignore[arg-type] ) # Then @@ -120,7 +123,10 @@ def test_gitlab_webhook__missing_config__returns_404( url, data="{}", content_type="application/json", - **{"HTTP_X_GITLAB_TOKEN": "some-secret", "HTTP_X_GITLAB_EVENT": "Merge Request Hook"}, # type: ignore[arg-type] + **{ + "HTTP_X_GITLAB_TOKEN": "some-secret", + "HTTP_X_GITLAB_EVENT": "Merge Request Hook", + }, # type: ignore[arg-type] ) # Then