Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ const Constants = {
'FEATURE_ID': 150,
'SEGMENT_ID': 150,
'TRAITS_ID': 150,
'VARIANT_KEY': 255,
},
},

Expand Down Expand Up @@ -651,6 +652,7 @@ const Constants = {
'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.',
REMOTE_CONFIG_DESCRIPTION_VARIATION:
'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.',
RESERVED_VARIANT_KEY: 'control',
SEGMENT_OVERRIDES_DESCRIPTION:
'Set different values for your feature based on what segments users are in. Identity overrides will take priority over any segment override.',
TAGS_DESCRIPTION:
Expand Down
19 changes: 16 additions & 3 deletions frontend/common/providers/FeatureListProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,18 @@ const FeatureListProvider = class extends React.Component {
environmentFlag,
segmentOverrides,
) => {
AppActions.createFlag(projectId, environmentId, flag, segmentOverrides)
AppActions.createFlag(
projectId,
environmentId,
{
...flag,
multivariate_options: flag.multivariate_options?.map((v, i) => ({
...v,
key: v.key || Utils.getDefaultVariantKey(i),
})),
},
segmentOverrides,
)
}

editFeatureValue = (
Expand All @@ -94,7 +105,7 @@ const FeatureListProvider = class extends React.Component {
Object.assign({}, projectFlag, {
multivariate_options:
flag.multivariate_options &&
flag.multivariate_options.map((v) => {
flag.multivariate_options.map((v, i) => {
const matchingProjectVariate =
(projectFlag.multivariate_options &&
projectFlag.multivariate_options.find((p) => p.id === v.id)) ||
Expand All @@ -103,6 +114,7 @@ const FeatureListProvider = class extends React.Component {
...v,
default_percentage_allocation:
matchingProjectVariate.default_percentage_allocation,
key: v.key || Utils.getDefaultVariantKey(i),
}
}),
}),
Expand Down Expand Up @@ -192,7 +204,7 @@ const FeatureListProvider = class extends React.Component {
Object.assign({}, projectFlag, flag, {
multivariate_options:
flag.multivariate_options &&
flag.multivariate_options.map((v) => {
flag.multivariate_options.map((v, i) => {
const matchingProjectVariate =
(projectFlag.multivariate_options &&
projectFlag.multivariate_options.find((p) => p.id === v.id)) ||
Expand All @@ -201,6 +213,7 @@ const FeatureListProvider = class extends React.Component {
...v,
default_percentage_allocation:
matchingProjectVariate.default_percentage_allocation,
key: v.key || Utils.getDefaultVariantKey(i),
}
}),
}),
Expand Down
132 changes: 132 additions & 0 deletions frontend/common/services/useMultivariateOption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { MultivariateOption, ProjectFlag, Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'

export const multivariateOptionService = service.injectEndpoints({
endpoints: (builder) => ({
createMultivariateOption: builder.mutation<
Res['multivariateOption'],
Req['createMultivariateOption']
>({
query: (query) => ({
body: query.body,
method: 'POST',
url: `projects/${query.project_id}/features/${query.feature_id}/mv-options/`,
}),
}),
saveMultivariateOptions: builder.mutation<
Res['saveMultivariateOptions'],
Req['saveMultivariateOptions']
>({
// No invalidatesTags: every save chain already ends with a broad
// invalidateTags(['ProjectFlag', 'FeatureList']) once the downstream
// feature-state save completes — invalidating here too would refetch
// every subscribed query twice per save.
queryFn: async (args, _, _2, baseQuery) => {
const featureUrl = `projects/${args.project_id}/features/${args.feature_id}/`
// Diff against the server's current options rather than any client
// cache, so stale state can never turn an update into a duplicate
// create.
const flagRes = await baseQuery({ method: 'GET', url: featureUrl })
if (flagRes.error) {
return { error: flagRes.error }
}
const serverOptions =
(flagRes.data as ProjectFlag)?.multivariate_options || []
const errors: Record<number, any> = {}
// Results are written back by input index — downstream feature
// state saves map weights to option ids positionally. Requests run
// sequentially so newly created options get ascending ids in input
// order, which is the order the UI displays.
const ordered: MultivariateOption[] = []
for (let i = 0; i < args.multivariate_options.length; i++) {
const v = args.multivariate_options[i]
let original
if (v.id) {
original = serverOptions.find((m) => m.id === v.id)
} else if (v.key) {
original = serverOptions.find((m) => !!m.key && m.key === v.key)
}
const body = {
...v,
default_percentage_allocation: 0,
feature: args.feature_id,
}
const res = await baseQuery(
original
? {
body,
method: 'PUT',
url: `${featureUrl}mv-options/${original.id}/`,
}
: {
body,
method: 'POST',
url: `${featureUrl}mv-options/`,
},
)
if (res.error) {
errors[i] = (res.error as { data?: any })?.data ?? null
} else {
ordered[i] = res.data as MultivariateOption
}
}
if (Object.keys(errors).length) {
return { data: { errors, multivariate_options: ordered } }
}
const deleted = serverOptions.filter(
(m) => !ordered.find((o) => o?.id === m.id),
)
const deleteResults = await Promise.all(
deleted.map((m) =>
baseQuery({
method: 'DELETE',
url: `${featureUrl}mv-options/${m.id}/`,
}),
),
)
const failedDelete = deleteResults.find((r) => r.error)
if (failedDelete) {
return { error: failedDelete.error }
}
return { data: { errors: null, multivariate_options: ordered } }
},
}),
// END OF ENDPOINTS
}),
})

export async function createMultivariateOption(
store: any,
data: Req['createMultivariateOption'],
options?: Parameters<
typeof multivariateOptionService.endpoints.createMultivariateOption.initiate
>[1],
) {
return store.dispatch(
multivariateOptionService.endpoints.createMultivariateOption.initiate(
data,
options,
),
)
}
export async function saveMultivariateOptions(
store: any,
data: Req['saveMultivariateOptions'],
options?: Parameters<
typeof multivariateOptionService.endpoints.saveMultivariateOptions.initiate
>[1],
) {
return store.dispatch(
multivariateOptionService.endpoints.saveMultivariateOptions.initiate(
data,
options,
),
)
}

export const {
useCreateMultivariateOptionMutation,
useSaveMultivariateOptionsMutation,
// END OF EXPORTS
} = multivariateOptionService
13 changes: 13 additions & 0 deletions frontend/common/services/useProjectFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PagedResponse, ProjectFlag, Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'
import Utils from 'common/utils/utils'
import { sortMultivariateOptions } from 'common/utils/multivariate'

/**
* Number of features to display per page in the features list.
Expand Down Expand Up @@ -122,6 +123,12 @@ export const projectFlagService = service
pageSize: arg.page_size || FEATURES_PAGE_SIZE,
previous: response.previous,
},
results: response.results.map((feature) => ({
...feature,
multivariate_options: sortMultivariateOptions(
feature.multivariate_options,
),
})),
}),
}),

Expand All @@ -130,6 +137,12 @@ export const projectFlagService = service
query: (query: Req['getProjectFlag']) => ({
url: `projects/${query.project}/features/${query.id}/`,
}),
transformResponse: (res: Res['projectFlag']) => ({
...res,
multivariate_options: sortMultivariateOptions(
res.multivariate_options,
),
}),
}),

getProjectFlags: builder.query<
Expand Down
Loading
Loading