Skip to content

Commit e2ce05f

Browse files
committed
feat: Create Release Pipeline
1 parent 31bd046 commit e2ce05f

13 files changed

Lines changed: 604 additions & 4 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// TODO: Add types
2+
// import { Res } from 'common/types/responses'
3+
// import { Req } from 'common/types/requests'
4+
import { service } from 'common/service'
5+
6+
export const releasePipelinesService = service
7+
.enhanceEndpoints({ addTagTypes: ['ReleasePipelines'] })
8+
.injectEndpoints({
9+
endpoints: (builder) => ({
10+
// Res['releasePipelines'], Req['getReleasePipelines']
11+
getReleasePipelines: builder.query<any, any>({
12+
providesTags: [{ id: 'LIST', type: 'ReleasePipelines' }],
13+
queryFn: async (query, api, extraOptions, baseQuery) => {
14+
await new Promise((resolve) => {
15+
setTimeout(() => {
16+
resolve({
17+
data: {
18+
results: [],
19+
},
20+
})
21+
}, 1500)
22+
})
23+
24+
return {
25+
data: {
26+
results: [],
27+
},
28+
}
29+
},
30+
}),
31+
// END OF ENDPOINTS
32+
}),
33+
})
34+
35+
export async function getReleasePipelines(
36+
store: any,
37+
// data: Req['getReleasePipelines'],
38+
data: any,
39+
options?: Parameters<
40+
typeof releasePipelinesService.endpoints.getReleasePipelines.initiate
41+
>[1],
42+
) {
43+
return store.dispatch(
44+
releasePipelinesService.endpoints.getReleasePipelines.initiate(
45+
data,
46+
options,
47+
),
48+
)
49+
}
50+
// END OF FUNCTION_EXPORTS
51+
52+
export const {
53+
useGetReleasePipelinesQuery,
54+
// END OF EXPORTS
55+
} = releasePipelinesService
56+
57+
/* Usage examples:
58+
const { data, isLoading } = useGetReleasePipelinesQuery({ id: 2 }, {}) //get hook
59+
const [createReleasePipelines, { isLoading, data, isSuccess }] = useCreateReleasePipelinesMutation() //create hook
60+
releasePipelinesService.endpoints.getReleasePipelines.select({id: 2})(store.getState()) //access data from any function
61+
*/

frontend/common/utils/utils.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ const Utils = Object.assign({}, require('./base/_utils'), {
110110
},
111111

112112
capitalize(str: string) {
113-
if (!str) return ""
113+
if (!str) return ''
114114
return str.charAt(0).toUpperCase() + str.slice(1)
115115
},
116116

@@ -673,6 +673,14 @@ const Utils = Object.assign({}, require('./base/_utils'), {
673673
.replace(/[\s_]+/g, '-')
674674
.toLowerCase(),
675675

676+
toSelectedValue: (
677+
value: string,
678+
options: { label: string; value: string }[],
679+
defaultValue?: string,
680+
) => {
681+
return options?.find((option) => option.value === value) ?? defaultValue
682+
},
683+
676684
validateMetadataType(type: string, value: any) {
677685
switch (type) {
678686
case 'int': {

frontend/web/components/App.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,23 @@ const App = class extends Component {
613613
)
614614
}
615615
</Permission>
616+
<Permission
617+
level='project'
618+
permission='ADMIN'
619+
id={projectId}
620+
>
621+
{({ permission }) =>
622+
permission && (
623+
<NavSubLink
624+
icon={<Icon name='flash' />}
625+
id='release-pipelines-link'
626+
to={`/project/${projectId}/release-pipelines`}
627+
>
628+
Release Pipelines
629+
</NavSubLink>
630+
)
631+
}
632+
</Permission>
616633
</>
617634
) : (
618635
!!AccountStore.getOrganisation() && (
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import CreateReleasePipeline from 'components/release-pipelines/CreateReleasePipeline'
2+
3+
type CreateReleasePipelinePageType = {
4+
match: {
5+
params: {
6+
projectId: string
7+
}
8+
}
9+
}
10+
11+
export default function CreateReleasePipelinePage({
12+
match,
13+
}: CreateReleasePipelinePageType) {
14+
const { projectId } = match.params
15+
return <CreateReleasePipeline projectId={projectId} />
16+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import PageTitle from 'components/PageTitle'
2+
import { useGetReleasePipelinesQuery } from 'common/services/useReleasePipelines'
3+
import { Button } from 'components/base/forms/Button'
4+
import { RouterChildContext } from 'react-router'
5+
import ConfigProvider from 'common/providers/ConfigProvider'
6+
7+
type ReleasePipelinesPageType = {
8+
router: RouterChildContext['router']
9+
match: {
10+
params: {
11+
environmentId: string
12+
projectId: string
13+
}
14+
}
15+
}
16+
17+
const NoReleasePipelines = ({
18+
projectId,
19+
router,
20+
}: {
21+
router: RouterChildContext['router']
22+
projectId: string
23+
}) => {
24+
return (
25+
<div className='mt-5 text-center'>
26+
<p>
27+
Create release pipelines to automate and standardize your release
28+
process throughout your organization.
29+
</p>
30+
<Row className='align-items-center justify-content-center gap-3'>
31+
<Button
32+
onClick={() =>
33+
router.history.push(
34+
`/project/${projectId}/release-pipelines/create`,
35+
)
36+
}
37+
>
38+
Create release pipeline
39+
</Button>
40+
<Button theme='outline'>Learn more</Button>
41+
</Row>
42+
</div>
43+
)
44+
}
45+
46+
type ReleasePipelinesPageContentProps = {
47+
data: any // TODO: type ReleasePipeline[]
48+
isLoading: boolean
49+
projectId: string
50+
router: RouterChildContext['router']
51+
}
52+
53+
const ReleasePipelinesPageContent = ({
54+
data,
55+
isLoading,
56+
projectId,
57+
router,
58+
}: ReleasePipelinesPageContentProps) => {
59+
if (isLoading) {
60+
return (
61+
<div className='text-center'>
62+
<Loader />
63+
</div>
64+
)
65+
}
66+
67+
if (!data?.results?.length) {
68+
return <NoReleasePipelines router={router} projectId={projectId} />
69+
}
70+
71+
return <div>Release Pipelines List</div>
72+
}
73+
74+
const ReleasePipelinesPage = ({ match, router }: ReleasePipelinesPageType) => {
75+
const { projectId } = match.params
76+
const { data, isLoading } = useGetReleasePipelinesQuery({})
77+
const hasReleasePipelines = !!data?.results?.length
78+
79+
return (
80+
<div className='app-container container'>
81+
<PageTitle
82+
title={'Release Pipelines'}
83+
cta={hasReleasePipelines && <Button>Create release pipeline</Button>}
84+
>
85+
{hasReleasePipelines &&
86+
'Define the stages your flags should go from development to launched. Learn more.'}
87+
</PageTitle>
88+
<ReleasePipelinesPageContent
89+
data={data}
90+
isLoading={isLoading}
91+
projectId={projectId}
92+
router={router}
93+
/>
94+
</div>
95+
)
96+
}
97+
98+
export default ConfigProvider(ReleasePipelinesPage)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { useState } from 'react'
2+
import PipelineStage from './PipelineStage'
3+
import { StageData } from './PipelineStage'
4+
import StageLine from './StageLine'
5+
import Breadcrumb from 'components/Breadcrumb'
6+
import { Button } from 'components/base/forms/Button'
7+
import PageTitle from 'components/PageTitle'
8+
9+
type PipelineData = {
10+
status: 'draft' | 'active'
11+
stages: StageData[]
12+
}
13+
14+
type CreateReleasePipelineType = {
15+
projectId: string
16+
}
17+
18+
export default function CreateReleasePipeline({
19+
projectId,
20+
}: CreateReleasePipelineType) {
21+
const [pipelineData, setPipelineData] = useState<PipelineData>({
22+
stages: [
23+
{
24+
name: '',
25+
},
26+
],
27+
status: 'draft',
28+
})
29+
30+
const handleOnChange = (stageData: StageData, index: number) => {
31+
const newStageData = pipelineData.stages.map((stage, i) =>
32+
i === index ? stageData : stage,
33+
)
34+
setPipelineData({ ...pipelineData, stages: newStageData })
35+
}
36+
37+
const validatePipelineData = () => {
38+
const hasName = pipelineData.stages.every((stage) => stage.name !== '')
39+
const hasSegments = pipelineData.stages.every((stage) => !!stage.segment)
40+
return hasName && hasSegments
41+
}
42+
43+
return (
44+
<div className='app-container container'>
45+
<Breadcrumb
46+
items={[
47+
{
48+
title: 'Release Pipelines',
49+
url: `/project/${projectId}/release-pipelines`,
50+
},
51+
]}
52+
currentPage={'New Release Pipeline'}
53+
/>
54+
55+
<PageTitle
56+
title='New Release Pipeline'
57+
cta={
58+
<Button disabled={!validatePipelineData()} onClick={() => {}}>
59+
Save Release Pipeline
60+
</Button>
61+
}
62+
/>
63+
<div className='px-2 pb-4 overflow-auto'>
64+
<Row className='no-wrap'>
65+
{pipelineData.stages.map((stageData, index) => (
66+
<Row key={index}>
67+
<Row className='align-items-start no-wrap'>
68+
<PipelineStage
69+
stageData={stageData}
70+
onChange={(stageData) => handleOnChange(stageData, index)}
71+
projectId={projectId}
72+
/>
73+
<div className='flex-1'>
74+
<StageLine
75+
showAddStageButton={
76+
index === pipelineData.stages.length - 1
77+
}
78+
onAddStage={() =>
79+
setPipelineData((prev) => ({
80+
...prev,
81+
stages: [...prev.stages, { name: '' }],
82+
}))
83+
}
84+
/>
85+
</div>
86+
</Row>
87+
</Row>
88+
))}
89+
</Row>
90+
</div>
91+
</div>
92+
)
93+
}

0 commit comments

Comments
 (0)