Skip to content

Commit 68b4ac2

Browse files
asaphkoclaude
andcommitted
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) <noreply@anthropic.com>
1 parent 6bb151d commit 68b4ac2

18 files changed

+1003
-101
lines changed

frontend/common/constants.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,10 @@ const Constants = {
492492
githubIssue: 'GitHub Issue',
493493
githubPR: 'Github PR',
494494
},
495+
gitlabType: {
496+
gitlabIssue: 'GitLab Issue',
497+
gitlabMR: 'GitLab MR',
498+
},
495499
integrationCategoryDescriptions: {
496500
'All': 'Send data on what flags served to each identity.',
497501
'Analytics': 'Send data on what flags served to each identity.',
@@ -685,6 +689,18 @@ const Constants = {
685689
resourceType: 'pulls',
686690
type: 'GITHUB',
687691
},
692+
GITLAB_ISSUE: {
693+
id: 3,
694+
label: 'Issue',
695+
resourceType: 'issues',
696+
type: 'GITLAB',
697+
},
698+
GITLAB_MR: {
699+
id: 4,
700+
label: 'Merge Request',
701+
resourceType: 'merge_requests',
702+
type: 'GITLAB',
703+
},
688704
},
689705
roles: {
690706
'ADMIN': 'Organisation Administrator',
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useGetGitlabIntegrationQuery } from 'common/services/useGitlabIntegration'
2+
3+
export function useHasGitlabIntegration(projectId: number) {
4+
const { data } = useGetGitlabIntegrationQuery(
5+
{ project_id: projectId },
6+
{ skip: !projectId },
7+
)
8+
9+
return {
10+
hasIntegration: !!data?.results?.length,
11+
}
12+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Res } from 'common/types/responses'
2+
import { Req } from 'common/types/requests'
3+
import { service } from 'common/service'
4+
import Utils from 'common/utils/utils'
5+
6+
export const gitlabService = service
7+
.enhanceEndpoints({ addTagTypes: ['Gitlab'] })
8+
.injectEndpoints({
9+
endpoints: (builder) => ({
10+
getGitlabProjects: builder.query<
11+
Res['gitlabProjects'],
12+
Req['getGitlabProjects']
13+
>({
14+
providesTags: [{ id: 'LIST', type: 'Gitlab' }],
15+
query: (query: Req['getGitlabProjects']) => ({
16+
url: `projects/${query.project_id}/gitlab/projects/`,
17+
}),
18+
}),
19+
getGitlabResources: builder.query<
20+
Res['gitlabResources'],
21+
Req['getGitlabResources']
22+
>({
23+
providesTags: [{ id: 'LIST', type: 'Gitlab' }],
24+
query: (query: Req['getGitlabResources']) => ({
25+
url:
26+
`projects/${query.project_id}/gitlab/${query.gitlab_resource}/` +
27+
`?${Utils.toParam({
28+
gitlab_project_id: query.gitlab_project_id,
29+
page: query.page,
30+
page_size: query.page_size,
31+
project_name: query.project_name,
32+
})}`,
33+
}),
34+
}),
35+
// END OF ENDPOINTS
36+
}),
37+
})
38+
39+
export async function getGitlabResources(
40+
store: any,
41+
data: Req['getGitlabResources'],
42+
options?: Parameters<
43+
typeof gitlabService.endpoints.getGitlabResources.initiate
44+
>[1],
45+
) {
46+
return store.dispatch(
47+
gitlabService.endpoints.getGitlabResources.initiate(data, options),
48+
)
49+
}
50+
export async function getGitlabProjects(
51+
store: any,
52+
data: Req['getGitlabProjects'],
53+
options?: Parameters<
54+
typeof gitlabService.endpoints.getGitlabProjects.initiate
55+
>[1],
56+
) {
57+
return store.dispatch(
58+
gitlabService.endpoints.getGitlabProjects.initiate(data, options),
59+
)
60+
}
61+
// END OF FUNCTION_EXPORTS
62+
63+
export const {
64+
useGetGitlabProjectsQuery,
65+
useGetGitlabResourcesQuery,
66+
// END OF EXPORTS
67+
} = gitlabService
68+
69+
/* Usage examples:
70+
const { data, isLoading } = useGetGitlabResourcesQuery({ project_id: 2, gitlab_resource: 'issues', gitlab_project_id: 1, project_name: 'my-project' }, {}) //get hook
71+
const { data, isLoading } = useGetGitlabProjectsQuery({ project_id: 2 }, {}) //get hook
72+
gitlabService.endpoints.getGitlabProjects.select({ project_id: 2 })(store.getState()) //access data from any function
73+
*/
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Res } from 'common/types/responses'
2+
import { Req } from 'common/types/requests'
3+
import { service } from 'common/service'
4+
5+
export const gitlabIntegrationService = service
6+
.enhanceEndpoints({ addTagTypes: ['GitlabIntegration'] })
7+
.injectEndpoints({
8+
endpoints: (builder) => ({
9+
createGitlabIntegration: builder.mutation<
10+
Res['gitlabIntegrations'],
11+
Req['createGitlabIntegration']
12+
>({
13+
invalidatesTags: [{ id: 'LIST', type: 'GitlabIntegration' }],
14+
query: (query: Req['createGitlabIntegration']) => ({
15+
body: query.body,
16+
method: 'POST',
17+
url: `projects/${query.project_id}/integrations/gitlab/`,
18+
}),
19+
}),
20+
deleteGitlabIntegration: builder.mutation<
21+
Res['gitlabIntegrations'],
22+
Req['deleteGitlabIntegration']
23+
>({
24+
invalidatesTags: [{ id: 'LIST', type: 'GitlabIntegration' }],
25+
query: (query: Req['deleteGitlabIntegration']) => ({
26+
method: 'DELETE',
27+
url: `projects/${query.project_id}/integrations/gitlab/${query.gitlab_integration_id}/`,
28+
}),
29+
}),
30+
getGitlabIntegration: builder.query<
31+
Res['gitlabIntegrations'],
32+
Req['getGitlabIntegration']
33+
>({
34+
providesTags: [{ id: 'LIST', type: 'GitlabIntegration' }],
35+
query: (query: Req['getGitlabIntegration']) => ({
36+
url: `projects/${query.project_id}/integrations/gitlab/`,
37+
}),
38+
}),
39+
updateGitlabIntegration: builder.mutation<
40+
Res['gitlabIntegrations'],
41+
Req['updateGitlabIntegration']
42+
>({
43+
invalidatesTags: [{ id: 'LIST', type: 'GitlabIntegration' }],
44+
query: (query: Req['updateGitlabIntegration']) => ({
45+
body: query.body,
46+
method: 'PATCH',
47+
url: `projects/${query.project_id}/integrations/gitlab/${query.gitlab_integration_id}/`,
48+
}),
49+
}),
50+
// END OF ENDPOINTS
51+
}),
52+
})
53+
54+
export async function createGitlabIntegration(
55+
store: any,
56+
data: Req['createGitlabIntegration'],
57+
options?: Parameters<
58+
typeof gitlabIntegrationService.endpoints.createGitlabIntegration.initiate
59+
>[1],
60+
) {
61+
return store.dispatch(
62+
gitlabIntegrationService.endpoints.createGitlabIntegration.initiate(
63+
data,
64+
options,
65+
),
66+
)
67+
}
68+
export async function deleteGitlabIntegration(
69+
store: any,
70+
data: Req['deleteGitlabIntegration'],
71+
options?: Parameters<
72+
typeof gitlabIntegrationService.endpoints.deleteGitlabIntegration.initiate
73+
>[1],
74+
) {
75+
return store.dispatch(
76+
gitlabIntegrationService.endpoints.deleteGitlabIntegration.initiate(
77+
data,
78+
options,
79+
),
80+
)
81+
}
82+
export async function getGitlabIntegration(
83+
store: any,
84+
data: Req['getGitlabIntegration'],
85+
options?: Parameters<
86+
typeof gitlabIntegrationService.endpoints.getGitlabIntegration.initiate
87+
>[1],
88+
) {
89+
return store.dispatch(
90+
gitlabIntegrationService.endpoints.getGitlabIntegration.initiate(
91+
data,
92+
options,
93+
),
94+
)
95+
}
96+
export async function updateGitlabIntegration(
97+
store: any,
98+
data: Req['updateGitlabIntegration'],
99+
options?: Parameters<
100+
typeof gitlabIntegrationService.endpoints.updateGitlabIntegration.initiate
101+
>[1],
102+
) {
103+
return store.dispatch(
104+
gitlabIntegrationService.endpoints.updateGitlabIntegration.initiate(
105+
data,
106+
options,
107+
),
108+
)
109+
}
110+
// END OF FUNCTION_EXPORTS
111+
112+
export const {
113+
useCreateGitlabIntegrationMutation,
114+
useDeleteGitlabIntegrationMutation,
115+
useGetGitlabIntegrationQuery,
116+
useUpdateGitlabIntegrationMutation,
117+
// END OF EXPORTS
118+
} = gitlabIntegrationService

frontend/common/stores/default-flags.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,32 @@ const defaultFlags = {
8787
'tags': ['logging'],
8888
'title': 'Dynatrace',
8989
},
90+
'gitlab': {
91+
'description':
92+
'View your Flagsmith flags inside GitLab issues and merge requests.',
93+
'docs':
94+
'https://docs.flagsmith.com/integrations/project-management/gitlab',
95+
'fields': [
96+
{
97+
'default': 'https://gitlab.com',
98+
'key': 'gitlab_instance_url',
99+
'label': 'GitLab Instance URL',
100+
},
101+
{
102+
'hidden': true,
103+
'key': 'access_token',
104+
'label': 'Access Token',
105+
},
106+
{
107+
'key': 'webhook_secret',
108+
'label': 'Webhook Secret',
109+
},
110+
],
111+
'image': '/static/images/integrations/gitlab.svg',
112+
'isGitlabIntegration': true,
113+
'perEnvironment': false,
114+
'title': 'GitLab',
115+
},
90116
'grafana': {
91117
'description':
92118
'Receive Flagsmith annotations to your Grafana instance on feature flag and segment changes.',

frontend/common/types/requests.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,36 @@ export type Req = {
623623
github_resource: string
624624
}>
625625
getGithubRepos: { installation_id: string; organisation_id: number }
626+
// GitLab
627+
getGitlabIntegration: { project_id: number; id?: number }
628+
createGitlabIntegration: {
629+
project_id: number
630+
body: {
631+
gitlab_instance_url: string
632+
access_token: string
633+
webhook_secret: string
634+
}
635+
}
636+
updateGitlabIntegration: {
637+
project_id: number
638+
gitlab_integration_id: number
639+
body: {
640+
gitlab_project_id?: number
641+
project_name?: string
642+
tagging_enabled?: boolean
643+
}
644+
}
645+
deleteGitlabIntegration: {
646+
project_id: number
647+
gitlab_integration_id: number
648+
}
649+
getGitlabResources: PagedRequest<{
650+
project_id: number
651+
gitlab_project_id: number
652+
project_name: string
653+
gitlab_resource: string
654+
}>
655+
getGitlabProjects: { project_id: number }
626656
getServersideEnvironmentKeys: { environmentId: string }
627657
deleteServersideEnvironmentKeys: { environmentId: string; id: string }
628658
createServersideEnvironmentKeys: {

frontend/common/types/responses.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ export type IntegrationData = {
321321
image: string
322322
fields: IntegrationField[] | undefined
323323
isExternalInstallation: boolean
324+
isGitlabIntegration?: boolean
324325
perEnvironment: boolean
325326
title?: string
326327
organisation?: string
@@ -1162,6 +1163,26 @@ export type Res = {
11621163
githubRepository: PagedResponse<GithubRepository>
11631164
githubResources: GitHubPagedResponse<GithubResource>
11641165
githubRepos: GithubPaginatedRepos<Repository>
1166+
// GitLab
1167+
gitlabIntegration: {
1168+
id: number
1169+
gitlab_instance_url: string
1170+
webhook_secret: string
1171+
project: number
1172+
}
1173+
gitlabIntegrations: PagedResponse<Res['gitlabIntegration']>
1174+
GitlabResource: {
1175+
web_url: string
1176+
id: number
1177+
iid: number
1178+
title: string
1179+
state: string
1180+
merged: boolean
1181+
draft: boolean
1182+
}
1183+
gitlabResources: PagedResponse<Res['GitlabResource']>
1184+
GitlabProject: { id: number; name: string; path_with_namespace: string }
1185+
gitlabProjects: PagedResponse<Res['GitlabProject']>
11651186
segmentPriorities: {}
11661187
featureSegment: FeatureState['feature_segment']
11671188
featureVersions: PagedResponse<FeatureVersion>

frontend/common/utils/utils.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,12 @@ const Utils = Object.assign({}, require('./base/_utils'), {
377377
return 'identities'
378378
},
379379
getIntegrationData() {
380-
return Utils.getFlagsmithJSONValue(
380+
const data = Utils.getFlagsmithJSONValue(
381381
'integration_data',
382382
defaultFlags.integration_data,
383383
)
384+
// Merge default integration entries that may not be in the remote flag yet
385+
return { ...defaultFlags.integration_data, ...data }
384386
},
385387
getIsEdge() {
386388
const model = ProjectStore.model as null | ProjectType

0 commit comments

Comments
 (0)