Skip to content

Commit 118bdbc

Browse files
authored
feat: label variant key UI (#7745)
1 parent 3965016 commit 118bdbc

36 files changed

Lines changed: 1267 additions & 302 deletions

frontend/common/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ const Constants = {
396396
'FEATURE_ID': 150,
397397
'SEGMENT_ID': 150,
398398
'TRAITS_ID': 150,
399+
'VARIANT_KEY': 255,
399400
},
400401
},
401402

@@ -651,6 +652,7 @@ const Constants = {
651652
'Features can have values as well as being simply on or off, e.g. a font size for a banner or an environment variable for a server.',
652653
REMOTE_CONFIG_DESCRIPTION_VARIATION:
653654
'Features can have values as well as being simply on or off, e.g. a font size for a banner or an environment variable for a server.<br/>Variation values are set per project, the environment weight is per environment.',
655+
RESERVED_VARIANT_KEY: 'control',
654656
SEGMENT_OVERRIDES_DESCRIPTION:
655657
'Set different values for your feature based on what segments users are in. Identity overrides will take priority over any segment override.',
656658
TAGS_DESCRIPTION:

frontend/common/providers/FeatureListProvider.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,18 @@ const FeatureListProvider = class extends React.Component {
7979
environmentFlag,
8080
segmentOverrides,
8181
) => {
82-
AppActions.createFlag(projectId, environmentId, flag, segmentOverrides)
82+
AppActions.createFlag(
83+
projectId,
84+
environmentId,
85+
{
86+
...flag,
87+
multivariate_options: flag.multivariate_options?.map((v, i) => ({
88+
...v,
89+
key: v.key || Utils.getDefaultVariantKey(i),
90+
})),
91+
},
92+
segmentOverrides,
93+
)
8394
}
8495

8596
editFeatureValue = (
@@ -94,7 +105,7 @@ const FeatureListProvider = class extends React.Component {
94105
Object.assign({}, projectFlag, {
95106
multivariate_options:
96107
flag.multivariate_options &&
97-
flag.multivariate_options.map((v) => {
108+
flag.multivariate_options.map((v, i) => {
98109
const matchingProjectVariate =
99110
(projectFlag.multivariate_options &&
100111
projectFlag.multivariate_options.find((p) => p.id === v.id)) ||
@@ -103,6 +114,7 @@ const FeatureListProvider = class extends React.Component {
103114
...v,
104115
default_percentage_allocation:
105116
matchingProjectVariate.default_percentage_allocation,
117+
key: v.key || Utils.getDefaultVariantKey(i),
106118
}
107119
}),
108120
}),
@@ -192,7 +204,7 @@ const FeatureListProvider = class extends React.Component {
192204
Object.assign({}, projectFlag, flag, {
193205
multivariate_options:
194206
flag.multivariate_options &&
195-
flag.multivariate_options.map((v) => {
207+
flag.multivariate_options.map((v, i) => {
196208
const matchingProjectVariate =
197209
(projectFlag.multivariate_options &&
198210
projectFlag.multivariate_options.find((p) => p.id === v.id)) ||
@@ -201,6 +213,7 @@ const FeatureListProvider = class extends React.Component {
201213
...v,
202214
default_percentage_allocation:
203215
matchingProjectVariate.default_percentage_allocation,
216+
key: v.key || Utils.getDefaultVariantKey(i),
204217
}
205218
}),
206219
}),
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { MultivariateOption, ProjectFlag, Res } from 'common/types/responses'
2+
import { Req } from 'common/types/requests'
3+
import { service } from 'common/service'
4+
5+
export const multivariateOptionService = service.injectEndpoints({
6+
endpoints: (builder) => ({
7+
createMultivariateOption: builder.mutation<
8+
Res['multivariateOption'],
9+
Req['createMultivariateOption']
10+
>({
11+
query: (query) => ({
12+
body: query.body,
13+
method: 'POST',
14+
url: `projects/${query.project_id}/features/${query.feature_id}/mv-options/`,
15+
}),
16+
}),
17+
saveMultivariateOptions: builder.mutation<
18+
Res['saveMultivariateOptions'],
19+
Req['saveMultivariateOptions']
20+
>({
21+
// No invalidatesTags: every save chain already ends with a broad
22+
// invalidateTags(['ProjectFlag', 'FeatureList']) once the downstream
23+
// feature-state save completes — invalidating here too would refetch
24+
// every subscribed query twice per save.
25+
queryFn: async (args, _, _2, baseQuery) => {
26+
const featureUrl = `projects/${args.project_id}/features/${args.feature_id}/`
27+
// Diff against the server's current options rather than any client
28+
// cache, so stale state can never turn an update into a duplicate
29+
// create.
30+
const flagRes = await baseQuery({ method: 'GET', url: featureUrl })
31+
if (flagRes.error) {
32+
return { error: flagRes.error }
33+
}
34+
const serverOptions =
35+
(flagRes.data as ProjectFlag)?.multivariate_options || []
36+
const errors: Record<number, any> = {}
37+
// Results are written back by input index — downstream feature
38+
// state saves map weights to option ids positionally. Requests run
39+
// sequentially so newly created options get ascending ids in input
40+
// order, which is the order the UI displays.
41+
const ordered: MultivariateOption[] = []
42+
for (let i = 0; i < args.multivariate_options.length; i++) {
43+
const v = args.multivariate_options[i]
44+
let original
45+
if (v.id) {
46+
original = serverOptions.find((m) => m.id === v.id)
47+
} else if (v.key) {
48+
original = serverOptions.find((m) => !!m.key && m.key === v.key)
49+
}
50+
const body = {
51+
...v,
52+
default_percentage_allocation: 0,
53+
feature: args.feature_id,
54+
}
55+
const res = await baseQuery(
56+
original
57+
? {
58+
body,
59+
method: 'PUT',
60+
url: `${featureUrl}mv-options/${original.id}/`,
61+
}
62+
: {
63+
body,
64+
method: 'POST',
65+
url: `${featureUrl}mv-options/`,
66+
},
67+
)
68+
if (res.error) {
69+
errors[i] = (res.error as { data?: any })?.data ?? null
70+
} else {
71+
ordered[i] = res.data as MultivariateOption
72+
}
73+
}
74+
if (Object.keys(errors).length) {
75+
return { data: { errors, multivariate_options: ordered } }
76+
}
77+
const deleted = serverOptions.filter(
78+
(m) => !ordered.find((o) => o?.id === m.id),
79+
)
80+
const deleteResults = await Promise.all(
81+
deleted.map((m) =>
82+
baseQuery({
83+
method: 'DELETE',
84+
url: `${featureUrl}mv-options/${m.id}/`,
85+
}),
86+
),
87+
)
88+
const failedDelete = deleteResults.find((r) => r.error)
89+
if (failedDelete) {
90+
return { error: failedDelete.error }
91+
}
92+
return { data: { errors: null, multivariate_options: ordered } }
93+
},
94+
}),
95+
// END OF ENDPOINTS
96+
}),
97+
})
98+
99+
export async function createMultivariateOption(
100+
store: any,
101+
data: Req['createMultivariateOption'],
102+
options?: Parameters<
103+
typeof multivariateOptionService.endpoints.createMultivariateOption.initiate
104+
>[1],
105+
) {
106+
return store.dispatch(
107+
multivariateOptionService.endpoints.createMultivariateOption.initiate(
108+
data,
109+
options,
110+
),
111+
)
112+
}
113+
export async function saveMultivariateOptions(
114+
store: any,
115+
data: Req['saveMultivariateOptions'],
116+
options?: Parameters<
117+
typeof multivariateOptionService.endpoints.saveMultivariateOptions.initiate
118+
>[1],
119+
) {
120+
return store.dispatch(
121+
multivariateOptionService.endpoints.saveMultivariateOptions.initiate(
122+
data,
123+
options,
124+
),
125+
)
126+
}
127+
128+
export const {
129+
useCreateMultivariateOptionMutation,
130+
useSaveMultivariateOptionsMutation,
131+
// END OF EXPORTS
132+
} = multivariateOptionService

frontend/common/services/useProjectFlag.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { PagedResponse, ProjectFlag, Res } from 'common/types/responses'
22
import { Req } from 'common/types/requests'
33
import { service } from 'common/service'
44
import Utils from 'common/utils/utils'
5+
import { sortMultivariateOptions } from 'common/utils/multivariate'
56

67
/**
78
* Number of features to display per page in the features list.
@@ -122,6 +123,12 @@ export const projectFlagService = service
122123
pageSize: arg.page_size || FEATURES_PAGE_SIZE,
123124
previous: response.previous,
124125
},
126+
results: response.results.map((feature) => ({
127+
...feature,
128+
multivariate_options: sortMultivariateOptions(
129+
feature.multivariate_options,
130+
),
131+
})),
125132
}),
126133
}),
127134

@@ -130,6 +137,12 @@ export const projectFlagService = service
130137
query: (query: Req['getProjectFlag']) => ({
131138
url: `projects/${query.project}/features/${query.id}/`,
132139
}),
140+
transformResponse: (res: Res['projectFlag']) => ({
141+
...res,
142+
multivariate_options: sortMultivariateOptions(
143+
res.multivariate_options,
144+
),
145+
}),
133146
}),
134147

135148
getProjectFlags: builder.query<

0 commit comments

Comments
 (0)