Skip to content

Commit 7c972a6

Browse files
tiagoapoloZaimwa9kyle-ssg
authored
feat: wait for trigger summary ui update (#5956)
Co-authored-by: wadii <wadii.zaim@flagsmith.com> Co-authored-by: kyle-ssg <kyle@solidstategroup.com>
1 parent 49ff723 commit 7c972a6

13 files changed

Lines changed: 437 additions & 115 deletions

frontend/common/types/responses.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,15 @@ export interface StageAction {
873873

874874
export type Features = {
875875
[id: number]: {
876-
name: string
876+
created_at?: string
877+
phased_rollout_state?: {
878+
current_split: number
879+
increase_by: number
880+
increase_every: string
881+
initial_split: number
882+
is_rollout_complete: boolean
883+
last_updated_at: string
884+
}
877885
}
878886
}
879887

frontend/package-lock.json

Lines changed: 1 addition & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/web/components/Icon.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { FC } from 'react'
2-
import { fi } from 'date-fns/locale'
32

43
export type IconName =
54
| 'arrow-left'
@@ -56,6 +55,7 @@ export type IconName =
5655
| 'pr-draft'
5756
| 'pr-linked'
5857
| 'pr-merged'
58+
| 'radio'
5959
| 'refresh'
6060
| 'request'
6161
| 'required'
@@ -1488,6 +1488,28 @@ const Icon: FC<IconType> = ({ fill, fill2, height, name, width, ...rest }) => {
14881488
</svg>
14891489
)
14901490
}
1491+
1492+
case 'radio': {
1493+
return (
1494+
<svg
1495+
viewBox='0 0 24 24'
1496+
height={height ?? '24'}
1497+
width={width ?? '24'}
1498+
fill={fill ?? '#000000'}
1499+
xmlns='http://www.w3.org/2000/svg'
1500+
>
1501+
<g id='Outlined/circle'>
1502+
<path
1503+
id='Icon'
1504+
fillRule='evenodd'
1505+
clipRule='evenodd'
1506+
d='M12 4C7.589 4 4 7.589 4 12C4 16.411 7.589 20 12 20C16.411 20 20 16.411 20 12C20 7.589 16.411 4 12 4ZM12 22C6.486 22 2 17.514 2 12C2 6.486 6.486 2 12 2C17.514 2 22 6.486 22 12C22 17.514 17.514 22 12 22Z'
1507+
/>
1508+
</g>
1509+
</svg>
1510+
)
1511+
}
1512+
14911513
default:
14921514
return null
14931515
}

frontend/web/components/release-pipelines/CreateReleasePipeline.tsx

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useState } from 'react'
1+
import { useCallback, useEffect, useRef, useState } from 'react'
22
import CreatePipelineStage from './CreatePipelineStage'
33
import Breadcrumb from 'components/Breadcrumb'
44
import { Button } from 'components/base/forms/Button'
@@ -64,7 +64,8 @@ function CreateReleasePipeline() {
6464
},
6565
] = useUpdateReleasePipelineMutation()
6666

67-
const [publishReleasePipeline] = usePublishReleasePipelineMutation()
67+
const [publishReleasePipeline, { isLoading: isPublishingPipeline }] =
68+
usePublishReleasePipelineMutation()
6869

6970
const [pipelineData, setPipelineData] = useState<ReleasePipelineRequest>({
7071
name: '',
@@ -75,19 +76,22 @@ function CreateReleasePipeline() {
7576
const [isEditingName, setIsEditingName] = useState(
7677
!pipelineData?.name?.length,
7778
)
79+
const skipNextCreateToast = useRef(false)
7880

7981
const handleSuccess = useCallback(
80-
(updated = false) => {
82+
(updated = false, skipToast = false) => {
8183
history.push(`/project/${projectId}/release-pipelines`)
82-
toast(`Release pipeline ${updated ? 'updated' : 'created'} successfully`)
84+
if (!skipToast) {
85+
toast(
86+
`Release pipeline ${updated ? 'updated' : 'created'} successfully`,
87+
)
88+
}
8389
},
8490
[history, projectId],
8591
)
8692

87-
const [isSavingAndPublishing, setIsSavingAndPublishing] = useState(false)
88-
8993
useEffect(() => {
90-
if (isSavingAndPublishing) {
94+
if (skipNextCreateToast.current) {
9195
return
9296
}
9397

@@ -98,15 +102,10 @@ function CreateReleasePipeline() {
98102
if (isUpdatingPipelineSuccess) {
99103
return handleSuccess(true)
100104
}
101-
}, [
102-
isCreatingPipelineSuccess,
103-
isUpdatingPipelineSuccess,
104-
handleSuccess,
105-
isSavingAndPublishing,
106-
])
105+
}, [isCreatingPipelineSuccess, isUpdatingPipelineSuccess, handleSuccess])
107106

108107
useEffect(() => {
109-
if (isSavingAndPublishing) {
108+
if (skipNextCreateToast.current) {
110109
return
111110
}
112111

@@ -125,7 +124,6 @@ function CreateReleasePipeline() {
125124
createPipelineError,
126125
isUpdatingPipelineError,
127126
updatePipelineError,
128-
isSavingAndPublishing,
129127
])
130128

131129
useEffect(() => {
@@ -248,20 +246,20 @@ function CreateReleasePipeline() {
248246

249247
const handleSubmitAndPublish = async (id?: number) => {
250248
try {
251-
setIsSavingAndPublishing(true)
249+
skipNextCreateToast.current = true
252250
const response = await handleSubmit(id)
253251
await publishReleasePipeline({
254252
pipelineId: response.id,
255253
projectId: Number(projectId),
256-
})
257-
toast(`Release pipeline published successfully`)
254+
}).unwrap()
255+
toast('Release pipeline published successfully')
256+
handleSuccess(true, true)
257+
skipNextCreateToast.current = false
258258
} catch (error) {
259259
toast(
260260
`Error ${id ? 'updating' : 'creating'} and publishing pipeline`,
261261
'danger',
262262
)
263-
} finally {
264-
setIsSavingAndPublishing(false)
265263
}
266264
}
267265

@@ -271,7 +269,10 @@ function CreateReleasePipeline() {
271269
}
272270

273271
const isSaveDisabled =
274-
!validatePipelineData() || isCreatingPipeline || isUpdatingPipeline
272+
!validatePipelineData() ||
273+
isCreatingPipeline ||
274+
isUpdatingPipeline ||
275+
isPublishingPipeline
275276

276277
const saveButtonText = existingPipeline?.id ? 'Update' : 'Save'
277278

frontend/web/components/release-pipelines/FeaturePipelineStatus.tsx

Lines changed: 25 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,60 +3,9 @@ import {
33
useGetReleasePipelinesQuery,
44
} from 'common/services/useReleasePipelines'
55
import AccordionCard from 'components/base/accordion/AccordionCard'
6-
import ProjectStore from 'common/stores/project-store'
7-
import { Environment } from 'common/types/responses'
8-
import classNames from 'classnames'
96
import { useMemo } from 'react'
107

11-
interface StageStatusProps {
12-
featureId: number
13-
stageOrder: number
14-
stageName: string
15-
totalStages: number
16-
stageFeatures: number[]
17-
stageEnvironment?: number
18-
}
19-
20-
const StageStatus = ({
21-
featureId,
22-
stageEnvironment,
23-
stageFeatures,
24-
stageName,
25-
stageOrder,
26-
totalStages,
27-
}: StageStatusProps) => {
28-
const isInStage = stageFeatures?.includes(featureId) ?? false
29-
const showLine = totalStages > 1
30-
const lastStage = stageOrder === totalStages - 1
31-
const showLeftLine = showLine && stageOrder > 0
32-
const showRightLine = showLine && stageOrder !== totalStages - 1
33-
34-
const stageStyle = {
35-
marginRight: lastStage ? '0px' : '24px',
36-
}
37-
38-
const env = ProjectStore.getEnvironmentById(
39-
stageEnvironment,
40-
) as unknown as Environment
41-
const envName = env?.name
42-
43-
return (
44-
<div
45-
className='flex align-items-center gap-2 position-relative flex-1'
46-
style={stageStyle}
47-
>
48-
{showLeftLine && <div className='position-absolute line-left' />}
49-
{showRightLine && <div className='position-absolute line-right' />}
50-
<div
51-
className={classNames('circle-container', { 'in-stage': isInStage })}
52-
/>
53-
<span>
54-
<b>{stageName}</b>
55-
</span>
56-
{envName && <p className='text-muted'>{envName}</p>}
57-
</div>
58-
)
59-
}
8+
import StageStatus from './StageStatus'
609

6110
interface FeaturePipelineStatusProps {
6211
featureId: number
@@ -69,7 +18,9 @@ const FeaturePipelineStatus = ({
6918
}: FeaturePipelineStatusProps) => {
7019
const { data: releasePipelines } = useGetReleasePipelinesQuery(
7120
{
21+
page_size: 100,
7222
projectId: Number(projectId),
23+
q: 'name',
7324
},
7425
{
7526
skip: !projectId,
@@ -96,6 +47,20 @@ const FeaturePipelineStatus = ({
9647
const stages = releasePipeline?.stages
9748
const totalStages = (stages?.length ?? 0) + 1
9849

50+
const stageHasFeature = useMemo(
51+
() =>
52+
stages?.find((stage) => {
53+
const stageFeatureIds = Object.keys(stage.features ?? {})
54+
return stageFeatureIds.includes(featureId.toString())
55+
}),
56+
[stages, featureId],
57+
)
58+
59+
const featureInStage = useMemo(
60+
() => stageHasFeature?.features?.[featureId],
61+
[stageHasFeature, featureId],
62+
)
63+
9964
if (!stages) return null
10065

10166
return (
@@ -106,18 +71,22 @@ const FeaturePipelineStatus = ({
10671
key={stage.id}
10772
stageOrder={stage.order}
10873
stageName={stage.name}
109-
stageFeatures={stage.features}
11074
stageEnvironment={stage.environment}
75+
stageActions={stage.actions}
76+
stageTrigger={stage.trigger}
11177
totalStages={totalStages}
112-
featureId={featureId}
78+
featureInStage={
79+
stageHasFeature?.order === stage.order
80+
? featureInStage
81+
: undefined
82+
}
83+
isCompleted={stage.order < (stageHasFeature?.order ?? -1)}
11384
/>
11485
))}
11586
<StageStatus
11687
stageOrder={totalStages - 1}
11788
stageName='Done'
118-
stageFeatures={[]}
11989
totalStages={totalStages}
120-
featureId={featureId}
12190
/>
12291
</Row>
12392
</AccordionCard>

frontend/web/components/release-pipelines/FlagActionDetail.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { useGetSegmentQuery } from 'common/services/useSegment'
22
import { StageActionBody, StageActionType } from 'common/types/responses'
3+
import moment from 'moment'
34

45
type FlagActionDetailProps = {
56
actionType: StageActionType
67
actionBody: StageActionBody
78
projectId: number
89
}
910

10-
const renderActionDetail = (
11+
export const renderActionDetail = (
1112
actionType: StageActionType,
12-
enabled: boolean,
13+
actionBody: StageActionBody,
1314
segmentName?: string,
15+
currentSplit?: number,
1416
) => {
15-
const actionPrefixText = enabled ? 'Enable' : 'Disable'
17+
const actionPrefixText = actionBody.enabled ? 'Enable' : 'Disable'
1618
switch (actionType) {
1719
case StageActionType.TOGGLE_FEATURE_FOR_SEGMENT:
1820
return (
@@ -26,9 +28,41 @@ const renderActionDetail = (
2628
{actionPrefixText} flag for <b>everyone</b>
2729
</span>
2830
)
29-
case StageActionType.PHASED_ROLLOUT:
30-
// TODO: TBD
31-
return <span>Phased rollout</span>
31+
case StageActionType.PHASED_ROLLOUT: {
32+
const { increase_by, increase_every, initial_split } = actionBody
33+
return (
34+
<div>
35+
<div className='mb-1'>
36+
<b>{actionPrefixText}</b> flag with <b>phased rollout</b>.
37+
</div>
38+
{initial_split && (
39+
<div className='mb-1'>
40+
Initial split of <b>{initial_split}%</b>
41+
</div>
42+
)}
43+
{increase_by && increase_every && (
44+
<div className='mb-1'>
45+
Increase by <b>{increase_by}%</b> every{' '}
46+
<b>
47+
{moment
48+
.duration(increase_every)
49+
.humanize()
50+
.replace(/(a |in )/g, '')}
51+
.
52+
</b>
53+
</div>
54+
)}
55+
{currentSplit && (
56+
<div className='mb-1 fs-caption text-muted'>
57+
Current rollout:{' '}
58+
<b className={currentSplit ? 'text-success' : ''}>
59+
{currentSplit}%
60+
</b>
61+
</div>
62+
)}
63+
</div>
64+
)
65+
}
3266
default:
3367
return null
3468
}
@@ -51,7 +85,7 @@ const FlagActionDetail = ({
5185
},
5286
)
5387

54-
return renderActionDetail(actionType, actionBody.enabled, segmentData?.name)
88+
return renderActionDetail(actionType, actionBody, segmentData?.name)
5589
}
5690

5791
export default FlagActionDetail

0 commit comments

Comments
 (0)