-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
feat(schedule flag changes): tweak frontend #19488
Changes from all commits
9d277aa
7d49c4c
7d51dbc
2563319
8496142
f72db3d
1d5c0ac
2be18e8
473b9bd
92b4bad
59d9e00
232175b
845bce5
10132ef
8bff377
d00e33d
e317449
20b2a8e
2c7be9e
4cd2bcf
20fe30a
60fd6a2
7b0b30e
99099e2
a22e702
a5289e8
74f98dc
7978672
5c71062
60ea996
2cc0f5d
3286b74
ac1d83b
5fde0f0
f3c283a
5fe726e
5cc9720
0d78d7a
bc842e4
d6b8a59
96131cd
7ff2c44
30e47a6
7e418ba
acd5fc4
f096549
3059b90
049d4a2
2249402
5af6b0a
803418e
b4f6d2d
3e260ba
1a0f51a
6827222
4da809f
04a9bb5
710cdb6
edac981
c7f1a23
41da321
9cba2f1
1cde7db
c77f238
3c524ef
1ba4020
77dc563
7353f82
cf5862a
674cc7d
4056337
4f19320
846dfcf
dc05e1b
62340ed
679052d
c1466fe
ca435c2
b15f114
b00bc9a
300f720
7eb3a1e
0290b83
6c837af
588c3f6
2c10a83
e106542
ee11c1f
201baf7
335da0d
47a3f22
abb36bc
8539d7b
e15224f
9f10f3b
9687b85
0c56473
c66ddb4
dc18728
858a302
4dd2589
9c9a618
5cfc4c4
3677bb1
f09eaae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,39 +6,49 @@ import { | |
LemonTable, | ||
LemonTableColumn, | ||
LemonTableColumns, | ||
LemonTag, | ||
LemonTagType, | ||
} from '@posthog/lemon-ui' | ||
import { useActions, useValues } from 'kea' | ||
import { DatePicker } from 'lib/components/DatePicker' | ||
import { dayjs } from 'lib/dayjs' | ||
import { More } from 'lib/lemon-ui/LemonButton/More' | ||
import { atColumn, createdAtColumn, createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils' | ||
import { Tooltip } from 'lib/lemon-ui/Tooltip' | ||
import { useEffect } from 'react' | ||
|
||
import { ScheduledChangeType } from '~/types' | ||
import { groupsModel } from '~/models/groupsModel' | ||
import { ScheduledChangeOperationType, ScheduledChangeType } from '~/types' | ||
|
||
import { featureFlagLogic } from './featureFlagLogic' | ||
import { FeatureFlagReleaseConditions } from './FeatureFlagReleaseConditions' | ||
import { groupFilters } from './FeatureFlags' | ||
|
||
const featureFlagScheduleLogic = featureFlagLogic({ id: 'schedule' }) | ||
export const DAYJS_FORMAT = 'MMMM DD, YYYY h:mm A' | ||
|
||
export default function FeatureFlagSchedule(): JSX.Element { | ||
const { featureFlag, scheduledChanges, scheduledChangeField, scheduleDateMarker } = | ||
const { featureFlag, scheduledChanges, scheduledChangeOperation, scheduleDateMarker } = | ||
useValues(featureFlagScheduleLogic) | ||
const { | ||
setFeatureFlagId, | ||
setFeatureFlag, | ||
setAggregationGroupTypeIndex, | ||
loadScheduledChanges, | ||
createScheduledChange, | ||
deleteScheduledChange, | ||
setScheduleDateMarker, | ||
setScheduledChangeField, | ||
setScheduledChangeOperation, | ||
} = useActions(featureFlagScheduleLogic) | ||
const { aggregationLabel } = useValues(groupsModel) | ||
|
||
const featureFlagId = useValues(featureFlagLogic).featureFlag.id | ||
const aggregationGroupTypeIndex = useValues(featureFlagLogic).featureFlag.filters.aggregation_group_type_index | ||
|
||
useEffect(() => { | ||
// Set the feature flag ID from the main flag logic to the current logic | ||
setFeatureFlagId(featureFlagId) | ||
setAggregationGroupTypeIndex(aggregationGroupTypeIndex || null) | ||
|
||
loadScheduledChanges() | ||
}, []) | ||
|
@@ -47,35 +57,89 @@ export default function FeatureFlagSchedule(): JSX.Element { | |
{ | ||
title: 'Change', | ||
dataIndex: 'payload', | ||
render: (dataValue) => { | ||
return JSON.stringify(dataValue) | ||
render: function Render(_, scheduledChange: ScheduledChangeType) { | ||
const { payload } = scheduledChange | ||
|
||
if (payload.operation === ScheduledChangeOperationType.UpdateStatus) { | ||
const isEnabled = payload.value | ||
return ( | ||
<LemonTag type={isEnabled ? 'success' : 'default'} className="uppercase"> | ||
{isEnabled ? 'Enable' : 'Disable'} | ||
</LemonTag> | ||
) | ||
} else if (payload.operation === ScheduledChangeOperationType.AddReleaseCondition) { | ||
const releaseText = groupFilters(payload.value, undefined, aggregationLabel) | ||
return ( | ||
<div className="inline-flex leading-8"> | ||
<span className="mr-2"> | ||
<b>Add release condition:</b> | ||
</span> | ||
{typeof releaseText === 'string' && releaseText.startsWith('100% of') ? ( | ||
<LemonTag type="highlight">{releaseText}</LemonTag> | ||
) : ( | ||
releaseText | ||
)} | ||
</div> | ||
) | ||
} | ||
|
||
return JSON.stringify(payload) | ||
}, | ||
}, | ||
atColumn('scheduled_at', 'Scheduled at') as LemonTableColumn< | ||
ScheduledChangeType, | ||
keyof ScheduledChangeType | undefined | ||
>, | ||
atColumn('executed_at', 'Executed at') as LemonTableColumn< | ||
ScheduledChangeType, | ||
keyof ScheduledChangeType | undefined | ||
>, | ||
createdByColumn() as LemonTableColumn<ScheduledChangeType, keyof ScheduledChangeType | undefined>, | ||
createdAtColumn() as LemonTableColumn<ScheduledChangeType, keyof ScheduledChangeType | undefined>, | ||
createdByColumn() as LemonTableColumn<ScheduledChangeType, keyof ScheduledChangeType | undefined>, | ||
{ | ||
width: 0, | ||
render: function Render(_: any, scheduledChange: ScheduledChangeType) { | ||
title: 'Status', | ||
dataIndex: 'executed_at', | ||
render: function Render(_, scheduledChange: ScheduledChangeType) { | ||
const { executed_at, failure_reason } = scheduledChange | ||
|
||
function getStatus(): { type: LemonTagType; text: string } { | ||
if (failure_reason) { | ||
return { type: 'danger', text: 'Error' } | ||
} else if (executed_at) { | ||
return { type: 'completion', text: 'Complete' } | ||
} else { | ||
return { type: 'default', text: 'Scheduled' } | ||
} | ||
} | ||
const { type, text } = getStatus() | ||
return ( | ||
<More | ||
overlay={ | ||
<LemonButton | ||
status="danger" | ||
onClick={() => deleteScheduledChange(scheduledChange.id)} | ||
fullWidth | ||
> | ||
Delete scheduled change | ||
</LemonButton> | ||
<Tooltip | ||
title={ | ||
failure_reason | ||
? `Failed: ${failure_reason}` | ||
: executed_at && `Completed: ${dayjs(executed_at).format('MMMM D, YYYY h:mm A')}` | ||
} | ||
/> | ||
> | ||
<LemonTag type={type}> | ||
<b className="uppercase">{text}</b> | ||
</LemonTag>{' '} | ||
</Tooltip> | ||
) | ||
}, | ||
}, | ||
{ | ||
width: 0, | ||
render: function Render(_, scheduledChange: ScheduledChangeType) { | ||
return ( | ||
!scheduledChange.executed_at && ( | ||
<More | ||
overlay={ | ||
<LemonButton | ||
status="danger" | ||
onClick={() => deleteScheduledChange(scheduledChange.id)} | ||
fullWidth | ||
> | ||
Delete scheduled change | ||
</LemonButton> | ||
} | ||
/> | ||
) | ||
) | ||
}, | ||
}, | ||
|
@@ -91,30 +155,35 @@ export default function FeatureFlagSchedule(): JSX.Element { | |
<LemonSelect | ||
className="w-50" | ||
placeholder="Select variant" | ||
value={scheduledChangeField} | ||
onChange={(value) => setScheduledChangeField(value)} | ||
value={scheduledChangeOperation} | ||
onChange={(value) => setScheduledChangeOperation(value)} | ||
options={[ | ||
{ label: 'Change status', value: 'active' }, | ||
{ label: 'Add a condition', value: 'filters' }, | ||
{ label: 'Change status', value: ScheduledChangeOperationType.UpdateStatus }, | ||
{ label: 'Add a condition', value: ScheduledChangeOperationType.AddReleaseCondition }, | ||
]} | ||
/> | ||
</div> | ||
<div> | ||
<div className="font-semibold leading-6 h-6 mb-1">Date and time</div> | ||
<DatePicker | ||
disabledDate={(dateMarker) => { | ||
const now = new Date() | ||
return dateMarker.toDate().getTime() < now.getTime() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's not obvious that the date is in the past, need a disabled reason to explain 😅 |
||
}} | ||
value={scheduleDateMarker} | ||
onChange={(value) => setScheduleDateMarker(value)} | ||
className="h-10 w-60" | ||
allowClear={false} | ||
showTime | ||
showSecond={false} | ||
format={DAYJS_FORMAT} | ||
showNow={false} | ||
/> | ||
</div> | ||
</div> | ||
|
||
<div className="space-y-4"> | ||
{scheduledChangeField === 'active' && ( | ||
{scheduledChangeOperation === ScheduledChangeOperationType.UpdateStatus && ( | ||
<> | ||
<div className="border rounded p-4"> | ||
<LemonCheckbox | ||
|
@@ -129,7 +198,9 @@ export default function FeatureFlagSchedule(): JSX.Element { | |
</div> | ||
</> | ||
)} | ||
{scheduledChangeField === 'filters' && <FeatureFlagReleaseConditions usageContext="schedule" />} | ||
{scheduledChangeOperation === ScheduledChangeOperationType.AddReleaseCondition && ( | ||
<FeatureFlagReleaseConditions usageContext="schedule" /> | ||
)} | ||
<div className="flex items-center justify-end"> | ||
<LemonButton | ||
disabledReason={!scheduleDateMarker ? 'Select the scheduled date and time' : null} | ||
|
@@ -142,6 +213,7 @@ export default function FeatureFlagSchedule(): JSX.Element { | |
<LemonDivider className="" /> | ||
</div> | ||
<LemonTable | ||
rowClassName={(record) => (record.executed_at ? 'opacity-75' : '')} | ||
className="mt-8" | ||
loading={false} | ||
dataSource={scheduledChanges} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,6 +42,7 @@ import { | |
PropertyFilterType, | ||
PropertyOperator, | ||
RolloutConditionType, | ||
ScheduledChangeOperationType, | ||
ScheduledChangeType, | ||
Survey, | ||
SurveyQuestionType, | ||
|
@@ -233,7 +234,7 @@ export const featureFlagLogic = kea<featureFlagLogicType>([ | |
setFeatureFlagId: (id: number | null) => ({ id }), | ||
setCopyDestinationProject: (id: number | null) => ({ id }), | ||
setScheduleDateMarker: (dateMarker: any) => ({ dateMarker }), | ||
setScheduledChangeField: (changeType: string | null) => ({ changeType }), | ||
setScheduledChangeOperation: (changeType: string | null) => ({ changeType }), | ||
}), | ||
forms(({ actions, values }) => ({ | ||
featureFlag: { | ||
|
@@ -493,10 +494,10 @@ export const featureFlagLogic = kea<featureFlagLogicType>([ | |
setScheduleDateMarker: (_, { dateMarker }) => dateMarker, | ||
}, | ||
], | ||
scheduledChangeField: [ | ||
'active' as string | null, | ||
scheduledChangeOperation: [ | ||
ScheduledChangeOperationType.AddReleaseCondition as string | null, | ||
{ | ||
setScheduledChangeField: (_, { changeType }) => changeType, | ||
setScheduledChangeOperation: (_, { changeType }) => changeType, | ||
}, | ||
], | ||
}), | ||
|
@@ -657,16 +658,20 @@ export const featureFlagLogic = kea<featureFlagLogicType>([ | |
scheduledChange: { | ||
__default: {} as ScheduledChangeType, | ||
createScheduledChange: async () => { | ||
const { featureFlag, scheduledChangeField, scheduleDateMarker, currentTeamId } = values | ||
const { featureFlag, scheduledChangeOperation, scheduleDateMarker, currentTeamId } = values | ||
|
||
if (currentTeamId && scheduledChangeField) { | ||
const fields = { | ||
[ScheduledChangeOperationType.UpdateStatus]: 'active', | ||
[ScheduledChangeOperationType.AddReleaseCondition]: 'filters', | ||
} | ||
|
||
if (currentTeamId && scheduledChangeOperation) { | ||
const data = { | ||
record_id: values.featureFlag.id, | ||
model_name: 'FeatureFlag', | ||
operation: scheduledChangeField, | ||
payload: { | ||
field: scheduledChangeField, | ||
value: featureFlag[scheduledChangeField], | ||
operation: scheduledChangeOperation, | ||
value: featureFlag[fields[scheduledChangeOperation]], | ||
}, | ||
scheduled_at: scheduleDateMarker.toISOString(), | ||
} | ||
|
@@ -873,6 +878,11 @@ export const featureFlagLogic = kea<featureFlagLogicType>([ | |
if (scheduledChange && scheduledChange) { | ||
lemonToast.success('Change scheduled successfully') | ||
actions.loadScheduledChanges() | ||
actions.setFeatureFlag({ | ||
...values.featureFlag, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this happens only on the keyed logic with id 'schedule' right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. correct! |
||
filters: NEW_FLAG.filters, | ||
active: NEW_FLAG.active, | ||
}) | ||
} | ||
}, | ||
deleteScheduledChangeSuccess: ({ scheduledChange }) => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
keeping consistent, easier to read 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is
featureFlag
is already destructured fromfeatureFlagScheduleLogic
, hence accessing the id here directly with the dot notation 😬There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahh, I see, minor nit, you can rename it when destructuring to prevent conflicts
const { featureFlag: originalFlag } = useValues(...)
const id = originalFlag.id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh I didn't know this was possible, thanks!