mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-17 13:46:47 +02:00
fix: improve on variant ui
This commit is contained in:
parent
9c009627ba
commit
5cbfcf5f3b
5
frontend/src/__mocks__/react-mdl.js
vendored
5
frontend/src/__mocks__/react-mdl.js
vendored
@ -29,4 +29,9 @@ module.exports = {
|
|||||||
FooterDropDownSection: 'react-mdl-FooterDropDownSection',
|
FooterDropDownSection: 'react-mdl-FooterDropDownSection',
|
||||||
FooterSection: 'react-mdl-FooterSection',
|
FooterSection: 'react-mdl-FooterSection',
|
||||||
FooterLinkList: 'react-mdl-FooterLinkList',
|
FooterLinkList: 'react-mdl-FooterLinkList',
|
||||||
|
Tooltip: 'react-mdl-Tooltip',
|
||||||
|
Dialog: 'react-mdl-Dialog',
|
||||||
|
DialogTitle: 'react-mdl-DialogTitle',
|
||||||
|
DialogContent: 'react-mdl-DialogContent',
|
||||||
|
DialogActions: 'react-mdl-DialogActions',
|
||||||
};
|
};
|
||||||
|
@ -20,3 +20,13 @@ export const trim = value => {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function updateWeight(variants, totalWeight) {
|
||||||
|
const size = variants.length;
|
||||||
|
const percentage = parseInt((1 / size) * totalWeight);
|
||||||
|
|
||||||
|
variants.forEach(v => {
|
||||||
|
v.weight = percentage;
|
||||||
|
});
|
||||||
|
return variants;
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ exports[`renders correctly with with variants 1`] = `
|
|||||||
<section
|
<section
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"maxWidth": "700px",
|
||||||
"padding": "16px",
|
"padding": "16px",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -20,36 +21,19 @@ exports[`renders correctly with with variants 1`] = `
|
|||||||
>
|
>
|
||||||
getVariant()
|
getVariant()
|
||||||
</code>
|
</code>
|
||||||
method in the client SDK.
|
method in the Client SDK.
|
||||||
</p>
|
</p>
|
||||||
<p
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"backgroundColor": "rgba(255, 229, 255, 0.4)",
|
|
||||||
"padding": "5px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
The sum of variants weights needs to be a constant number to guarantee consistent hashing in the client implementations, this is why we will sometime allocate a few more percentages to the first variant if the sum is not exactly 100. In a final version of this feature it should be possible to the user to manually set the percentages for each variant.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href="#add-variant"
|
|
||||||
onClick={[Function]}
|
|
||||||
title="Add variant"
|
|
||||||
>
|
|
||||||
Add variant
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<form>
|
|
||||||
<table
|
<table
|
||||||
className="mdl-data-table mdl-shadow--2dp variantTable"
|
className="mdl-data-table mdl-shadow--2dp variantTable"
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
Name
|
Variant name
|
||||||
</th>
|
</th>
|
||||||
|
<th
|
||||||
|
className="labels"
|
||||||
|
/>
|
||||||
<th>
|
<th>
|
||||||
Weight
|
Weight
|
||||||
</th>
|
</th>
|
||||||
@ -65,14 +49,29 @@ exports[`renders correctly with with variants 1`] = `
|
|||||||
>
|
>
|
||||||
blue
|
blue
|
||||||
</td>
|
</td>
|
||||||
|
<td
|
||||||
|
className="labels"
|
||||||
|
>
|
||||||
|
|
||||||
|
<react-mdl-Chip
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "rgba(173, 216, 230, 0.2)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Overrides
|
||||||
|
</react-mdl-Chip>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
34
|
3.4
|
||||||
|
%
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="actions"
|
className="actions"
|
||||||
>
|
>
|
||||||
<react-mdl-IconButton
|
<react-mdl-IconButton
|
||||||
name="expand_more"
|
name="edit"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
/>
|
/>
|
||||||
<react-mdl-IconButton
|
<react-mdl-IconButton
|
||||||
@ -86,15 +85,21 @@ exports[`renders correctly with with variants 1`] = `
|
|||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
yellow
|
yellow
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="labels"
|
||||||
|
>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
33
|
3.3
|
||||||
|
%
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="actions"
|
className="actions"
|
||||||
>
|
>
|
||||||
<react-mdl-IconButton
|
<react-mdl-IconButton
|
||||||
name="expand_more"
|
name="edit"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
/>
|
/>
|
||||||
<react-mdl-IconButton
|
<react-mdl-IconButton
|
||||||
@ -109,14 +114,23 @@ exports[`renders correctly with with variants 1`] = `
|
|||||||
>
|
>
|
||||||
orange
|
orange
|
||||||
</td>
|
</td>
|
||||||
|
<td
|
||||||
|
className="labels"
|
||||||
|
>
|
||||||
|
<react-mdl-Chip>
|
||||||
|
Payload
|
||||||
|
</react-mdl-Chip>
|
||||||
|
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
33
|
3.3
|
||||||
|
%
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="actions"
|
className="actions"
|
||||||
>
|
>
|
||||||
<react-mdl-IconButton
|
<react-mdl-IconButton
|
||||||
name="expand_more"
|
name="edit"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
/>
|
/>
|
||||||
<react-mdl-IconButton
|
<react-mdl-IconButton
|
||||||
@ -128,64 +142,6 @@ exports[`renders correctly with with variants 1`] = `
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
|
||||||
<react-mdl-Button
|
|
||||||
icon="add"
|
|
||||||
primary={true}
|
|
||||||
raised={true}
|
|
||||||
ripple={true}
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<react-mdl-Icon
|
|
||||||
name="add"
|
|
||||||
/>
|
|
||||||
|
|
||||||
Save
|
|
||||||
</react-mdl-Button>
|
|
||||||
|
|
||||||
<react-mdl-Button
|
|
||||||
onClick={[MockFunction]}
|
|
||||||
type="cancel"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</react-mdl-Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders correctly with without variants 1`] = `
|
|
||||||
<section
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"padding": "16px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Variants allows you to return a variant object if the feature toggle is considered enabled for the current request. When using variants you should use the
|
|
||||||
|
|
||||||
<code
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"color": "navy",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
getVariant()
|
|
||||||
</code>
|
|
||||||
method in the client SDK.
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"backgroundColor": "rgba(255, 229, 255, 0.4)",
|
|
||||||
"padding": "5px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
The sum of variants weights needs to be a constant number to guarantee consistent hashing in the client implementations, this is why we will sometime allocate a few more percentages to the first variant if the sum is not exactly 100. In a final version of this feature it should be possible to the user to manually set the percentages for each variant.
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#add-variant"
|
href="#add-variant"
|
||||||
@ -195,39 +151,176 @@ exports[`renders correctly with without variants 1`] = `
|
|||||||
Add variant
|
Add variant
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<form>
|
<form
|
||||||
<p />
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<react-mdl-Dialog
|
||||||
|
className="modal"
|
||||||
|
open={false}
|
||||||
|
>
|
||||||
|
<react-mdl-DialogTitle
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "10px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<react-mdl-DialogContent
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"minHeight": "350px",
|
||||||
|
"padding": "10px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "red",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Variant name"
|
||||||
|
name="name"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder=""
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="name"
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<br />
|
||||||
|
<react-mdl-Tooltip
|
||||||
|
className="tooltip"
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
Passed to the variant object.
|
||||||
|
<br />
|
||||||
|
Can be anything (json, value, csv)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginBottom": "0",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
Payload
|
||||||
|
</strong>
|
||||||
|
<react-mdl-Icon
|
||||||
|
name="info"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</react-mdl-Tooltip>
|
||||||
|
<react-mdl-Grid
|
||||||
|
noSpacing={true}
|
||||||
|
>
|
||||||
|
<react-mdl-Cell
|
||||||
|
col={3}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mdl-textfield mdl-js-textfield mdl-textfield--floating-label is-dirty is-upgraded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
className="mdl-textfield__input"
|
||||||
|
name="type"
|
||||||
|
onChange={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "auto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="string"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="string"
|
||||||
|
>
|
||||||
|
string
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="json"
|
||||||
|
>
|
||||||
|
json
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="csv"
|
||||||
|
>
|
||||||
|
csv
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
className="mdl-textfield__label"
|
||||||
|
htmlFor="textfield-conextName"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</react-mdl-Cell>
|
||||||
|
<react-mdl-Cell
|
||||||
|
col={9}
|
||||||
|
>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Value"
|
||||||
|
name="value"
|
||||||
|
onChange={[Function]}
|
||||||
|
rows={1}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</react-mdl-Cell>
|
||||||
|
</react-mdl-Grid>
|
||||||
|
<a
|
||||||
|
href="#add-override"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<small>
|
||||||
|
Add override
|
||||||
|
</small>
|
||||||
|
</a>
|
||||||
|
</react-mdl-DialogContent>
|
||||||
|
<react-mdl-DialogActions>
|
||||||
<react-mdl-Button
|
<react-mdl-Button
|
||||||
icon="add"
|
colored={true}
|
||||||
primary={true}
|
|
||||||
raised={true}
|
raised={true}
|
||||||
ripple={true}
|
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<react-mdl-Icon
|
|
||||||
name="add"
|
|
||||||
/>
|
|
||||||
|
|
||||||
Save
|
Save
|
||||||
</react-mdl-Button>
|
</react-mdl-Button>
|
||||||
|
|
||||||
<react-mdl-Button
|
<react-mdl-Button
|
||||||
onClick={[MockFunction]}
|
onClick={[Function]}
|
||||||
type="cancel"
|
type="button"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</react-mdl-Button>
|
</react-mdl-Button>
|
||||||
</div>
|
</react-mdl-DialogActions>
|
||||||
|
</react-mdl-Dialog>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders correctly with without variants and no permissions 1`] = `
|
exports[`renders correctly with without variants 1`] = `
|
||||||
<section
|
<section
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"maxWidth": "700px",
|
||||||
"padding": "16px",
|
"padding": "16px",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,21 +337,410 @@ exports[`renders correctly with without variants and no permissions 1`] = `
|
|||||||
>
|
>
|
||||||
getVariant()
|
getVariant()
|
||||||
</code>
|
</code>
|
||||||
method in the client SDK.
|
method in the Client SDK.
|
||||||
</p>
|
</p>
|
||||||
<p
|
<table
|
||||||
|
className="mdl-data-table mdl-shadow--2dp variantTable"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Variant name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="labels"
|
||||||
|
/>
|
||||||
|
<th>
|
||||||
|
Weight
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="actions"
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody />
|
||||||
|
</table>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="#add-variant"
|
||||||
|
onClick={[Function]}
|
||||||
|
title="Add variant"
|
||||||
|
>
|
||||||
|
Add variant
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<react-mdl-Dialog
|
||||||
|
className="modal"
|
||||||
|
open={false}
|
||||||
|
>
|
||||||
|
<react-mdl-DialogTitle
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"backgroundColor": "rgba(255, 229, 255, 0.4)",
|
"padding": "10px",
|
||||||
"padding": "5px",
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<react-mdl-DialogContent
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"minHeight": "350px",
|
||||||
|
"padding": "10px",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
The sum of variants weights needs to be a constant number to guarantee consistent hashing in the client implementations, this is why we will sometime allocate a few more percentages to the first variant if the sum is not exactly 100. In a final version of this feature it should be possible to the user to manually set the percentages for each variant.
|
<p
|
||||||
</p>
|
style={
|
||||||
<form>
|
Object {
|
||||||
<p />
|
"color": "red",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Variant name"
|
||||||
|
name="name"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder=""
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="name"
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
|
<br />
|
||||||
|
<react-mdl-Tooltip
|
||||||
|
className="tooltip"
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
Passed to the variant object.
|
||||||
|
<br />
|
||||||
|
Can be anything (json, value, csv)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginBottom": "0",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
Payload
|
||||||
|
</strong>
|
||||||
|
<react-mdl-Icon
|
||||||
|
name="info"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</react-mdl-Tooltip>
|
||||||
|
<react-mdl-Grid
|
||||||
|
noSpacing={true}
|
||||||
|
>
|
||||||
|
<react-mdl-Cell
|
||||||
|
col={3}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mdl-textfield mdl-js-textfield mdl-textfield--floating-label is-dirty is-upgraded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
className="mdl-textfield__input"
|
||||||
|
name="type"
|
||||||
|
onChange={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "auto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="string"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="string"
|
||||||
|
>
|
||||||
|
string
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="json"
|
||||||
|
>
|
||||||
|
json
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="csv"
|
||||||
|
>
|
||||||
|
csv
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
className="mdl-textfield__label"
|
||||||
|
htmlFor="textfield-conextName"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</react-mdl-Cell>
|
||||||
|
<react-mdl-Cell
|
||||||
|
col={9}
|
||||||
|
>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Value"
|
||||||
|
name="value"
|
||||||
|
onChange={[Function]}
|
||||||
|
rows={1}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</react-mdl-Cell>
|
||||||
|
</react-mdl-Grid>
|
||||||
|
<a
|
||||||
|
href="#add-override"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<small>
|
||||||
|
Add override
|
||||||
|
</small>
|
||||||
|
</a>
|
||||||
|
</react-mdl-DialogContent>
|
||||||
|
<react-mdl-DialogActions>
|
||||||
|
<react-mdl-Button
|
||||||
|
colored={true}
|
||||||
|
raised={true}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</react-mdl-Button>
|
||||||
|
<react-mdl-Button
|
||||||
|
onClick={[Function]}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</react-mdl-Button>
|
||||||
|
</react-mdl-DialogActions>
|
||||||
|
</react-mdl-Dialog>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`renders correctly with without variants and no permissions 1`] = `
|
||||||
|
<section
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"maxWidth": "700px",
|
||||||
|
"padding": "16px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Variants allows you to return a variant object if the feature toggle is considered enabled for the current request. When using variants you should use the
|
||||||
|
|
||||||
|
<code
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "navy",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
getVariant()
|
||||||
|
</code>
|
||||||
|
method in the Client SDK.
|
||||||
|
</p>
|
||||||
|
<table
|
||||||
|
className="mdl-data-table mdl-shadow--2dp variantTable"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Variant name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="labels"
|
||||||
|
/>
|
||||||
|
<th>
|
||||||
|
Weight
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="actions"
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody />
|
||||||
|
</table>
|
||||||
|
<br />
|
||||||
|
<form
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<react-mdl-Dialog
|
||||||
|
className="modal"
|
||||||
|
open={false}
|
||||||
|
>
|
||||||
|
<react-mdl-DialogTitle
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "10px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<react-mdl-DialogContent
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"minHeight": "350px",
|
||||||
|
"padding": "10px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "red",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Variant name"
|
||||||
|
name="name"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder=""
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="name"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<react-mdl-Tooltip
|
||||||
|
className="tooltip"
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
Passed to the variant object.
|
||||||
|
<br />
|
||||||
|
Can be anything (json, value, csv)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginBottom": "0",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
Payload
|
||||||
|
</strong>
|
||||||
|
<react-mdl-Icon
|
||||||
|
name="info"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</react-mdl-Tooltip>
|
||||||
|
<react-mdl-Grid
|
||||||
|
noSpacing={true}
|
||||||
|
>
|
||||||
|
<react-mdl-Cell
|
||||||
|
col={3}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mdl-textfield mdl-js-textfield mdl-textfield--floating-label is-dirty is-upgraded"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
className="mdl-textfield__input"
|
||||||
|
name="type"
|
||||||
|
onChange={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "auto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="string"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="string"
|
||||||
|
>
|
||||||
|
string
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="json"
|
||||||
|
>
|
||||||
|
json
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="csv"
|
||||||
|
>
|
||||||
|
csv
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
className="mdl-textfield__label"
|
||||||
|
htmlFor="textfield-conextName"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</react-mdl-Cell>
|
||||||
|
<react-mdl-Cell
|
||||||
|
col={9}
|
||||||
|
>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Value"
|
||||||
|
name="value"
|
||||||
|
onChange={[Function]}
|
||||||
|
rows={1}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</react-mdl-Cell>
|
||||||
|
</react-mdl-Grid>
|
||||||
|
<a
|
||||||
|
href="#add-override"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<small>
|
||||||
|
Add override
|
||||||
|
</small>
|
||||||
|
</a>
|
||||||
|
</react-mdl-DialogContent>
|
||||||
|
<react-mdl-DialogActions>
|
||||||
|
<react-mdl-Button
|
||||||
|
colored={true}
|
||||||
|
raised={true}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</react-mdl-Button>
|
||||||
|
<react-mdl-Button
|
||||||
|
onClick={[Function]}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</react-mdl-Button>
|
||||||
|
</react-mdl-DialogActions>
|
||||||
|
</react-mdl-Dialog>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
|
@ -8,34 +8,14 @@ import { UPDATE_FEATURE } from '../../../../permissions';
|
|||||||
jest.mock('react-mdl');
|
jest.mock('react-mdl');
|
||||||
|
|
||||||
test('renders correctly with without variants', () => {
|
test('renders correctly with without variants', () => {
|
||||||
const featureToggle = {
|
|
||||||
name: 'Another',
|
|
||||||
description: "another's description",
|
|
||||||
enabled: false,
|
|
||||||
strategies: [
|
|
||||||
{
|
|
||||||
name: 'gradualRolloutRandom',
|
|
||||||
parameters: {
|
|
||||||
percentage: 50,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
createdAt: '2018-02-04T20:27:52.127Z',
|
|
||||||
};
|
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<UpdateVariant
|
<UpdateVariant
|
||||||
key={0}
|
key={0}
|
||||||
input={featureToggle}
|
variants={[]}
|
||||||
onCancel={jest.fn()}
|
|
||||||
features={[]}
|
|
||||||
setValue={jest.fn()}
|
|
||||||
addVariant={jest.fn()}
|
addVariant={jest.fn()}
|
||||||
removeVariant={jest.fn()}
|
removeVariant={jest.fn()}
|
||||||
updateVariant={jest.fn()}
|
updateVariant={jest.fn()}
|
||||||
onSubmit={jest.fn()}
|
|
||||||
onCancel={jest.fn()}
|
|
||||||
init={jest.fn()}
|
|
||||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@ -45,34 +25,14 @@ test('renders correctly with without variants', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('renders correctly with without variants and no permissions', () => {
|
test('renders correctly with without variants and no permissions', () => {
|
||||||
const featureToggle = {
|
|
||||||
name: 'Another',
|
|
||||||
description: "another's description",
|
|
||||||
enabled: false,
|
|
||||||
strategies: [
|
|
||||||
{
|
|
||||||
name: 'gradualRolloutRandom',
|
|
||||||
parameters: {
|
|
||||||
percentage: 50,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
createdAt: '2018-02-04T20:27:52.127Z',
|
|
||||||
};
|
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<UpdateVariant
|
<UpdateVariant
|
||||||
key={0}
|
key={0}
|
||||||
input={featureToggle}
|
variants={[]}
|
||||||
onCancel={jest.fn()}
|
|
||||||
features={[]}
|
|
||||||
setValue={jest.fn()}
|
|
||||||
addVariant={jest.fn()}
|
addVariant={jest.fn()}
|
||||||
removeVariant={jest.fn()}
|
removeVariant={jest.fn()}
|
||||||
updateVariant={jest.fn()}
|
updateVariant={jest.fn()}
|
||||||
onSubmit={jest.fn()}
|
|
||||||
onCancel={jest.fn()}
|
|
||||||
init={jest.fn()}
|
|
||||||
hasPermission={() => false}
|
hasPermission={() => false}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@ -124,16 +84,10 @@ test('renders correctly with with variants', () => {
|
|||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<UpdateVariant
|
<UpdateVariant
|
||||||
key={0}
|
key={0}
|
||||||
input={featureToggle}
|
variants={featureToggle.variants}
|
||||||
onCancel={jest.fn()}
|
|
||||||
features={[]}
|
|
||||||
setValue={jest.fn()}
|
|
||||||
addVariant={jest.fn()}
|
addVariant={jest.fn()}
|
||||||
removeVariant={jest.fn()}
|
removeVariant={jest.fn()}
|
||||||
updateVariant={jest.fn()}
|
updateVariant={jest.fn()}
|
||||||
onSubmit={jest.fn()}
|
|
||||||
onCancel={jest.fn()}
|
|
||||||
init={jest.fn()}
|
|
||||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
242
frontend/src/component/feature/variant/add-variant.jsx
Normal file
242
frontend/src/component/feature/variant/add-variant.jsx
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Textfield,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Grid,
|
||||||
|
Cell,
|
||||||
|
Tooltip,
|
||||||
|
Icon,
|
||||||
|
} from 'react-mdl';
|
||||||
|
import MySelect from '../form/select';
|
||||||
|
import { trim } from '../form/util';
|
||||||
|
import styles from './variant.scss';
|
||||||
|
import OverrideConfig from './override-config';
|
||||||
|
|
||||||
|
const payloadOptions = [
|
||||||
|
{ key: 'string', label: 'string' },
|
||||||
|
{ key: 'json', label: 'json' },
|
||||||
|
{ key: 'csv', label: 'csv' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const EMPTY_PAYLOAD = { type: 'string', value: '' };
|
||||||
|
|
||||||
|
function AddVariant({ showDialog, closeDialog, save, validateName, editVariant, title }) {
|
||||||
|
const [data, setData] = useState({});
|
||||||
|
const [payload, setPayload] = useState(EMPTY_PAYLOAD);
|
||||||
|
const [overrides, setOverrides] = useState([]);
|
||||||
|
const [error, setError] = useState({});
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
if (editVariant) {
|
||||||
|
setData({ name: editVariant.name });
|
||||||
|
if (editVariant.payload) {
|
||||||
|
setPayload(editVariant.payload);
|
||||||
|
}
|
||||||
|
if (editVariant.overrides) {
|
||||||
|
setOverrides(
|
||||||
|
editVariant.overrides.map(o => ({ contextName: o.contextName, values: o.values.join(', ') }))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setOverrides([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setData({});
|
||||||
|
setPayload(EMPTY_PAYLOAD);
|
||||||
|
setOverrides([]);
|
||||||
|
}
|
||||||
|
setError({});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clear();
|
||||||
|
}, [editVariant]);
|
||||||
|
|
||||||
|
const setName = e => {
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
[e.target.name]: trim(e.target.value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const validationError = validateName(data.name);
|
||||||
|
|
||||||
|
if (validationError) {
|
||||||
|
setError(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const variant = {
|
||||||
|
name: data.name,
|
||||||
|
payload: payload.value ? payload : undefined,
|
||||||
|
overrides: overrides
|
||||||
|
.map(o => ({
|
||||||
|
contextName: o.contextName,
|
||||||
|
values: o.values
|
||||||
|
.split(',')
|
||||||
|
.map(v => v.trim())
|
||||||
|
.filter(v => v),
|
||||||
|
}))
|
||||||
|
.filter(o => o.values && o.values.length > 0),
|
||||||
|
};
|
||||||
|
await save(variant);
|
||||||
|
clear();
|
||||||
|
closeDialog();
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error.error || 'Could not add variant';
|
||||||
|
setError({ general: msg });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPayload = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setPayload({
|
||||||
|
...payload,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
clear();
|
||||||
|
closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOverrideOption = index => e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOverrides(
|
||||||
|
overrides.map((o, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
o[e.target.name] = e.target.value;
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeOverrideOption = index => e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOverrides(overrides.filter((o, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddOverride = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOverrides([...overrides, ...[{ contextName: 'userId', values: '' }]]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={submit}>
|
||||||
|
<Dialog open={showDialog} className={styles.modal}>
|
||||||
|
<DialogTitle style={{ padding: '10px' }}>{title}</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent style={{ minHeight: '350px', padding: '10px' }}>
|
||||||
|
<p style={{ color: 'red' }}>{error.general}</p>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Variant name"
|
||||||
|
name="name"
|
||||||
|
placeholder=""
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={data.name}
|
||||||
|
error={error.name}
|
||||||
|
type="name"
|
||||||
|
onChange={setName}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<Tooltip
|
||||||
|
className={styles.tooltip}
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
Passed to the variant object. <br />
|
||||||
|
Can be anything (json, value, csv)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p style={{ marginBottom: '0' }}>
|
||||||
|
<strong>Payload </strong>
|
||||||
|
<Icon name="info" />
|
||||||
|
</p>
|
||||||
|
</Tooltip>
|
||||||
|
<Grid noSpacing>
|
||||||
|
<Cell col={3}>
|
||||||
|
<MySelect
|
||||||
|
name="type"
|
||||||
|
label="Type"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={payload.type}
|
||||||
|
options={payloadOptions}
|
||||||
|
onChange={onPayload}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={9}>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
rows={1}
|
||||||
|
label="Value"
|
||||||
|
name="value"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={payload.value}
|
||||||
|
onChange={onPayload}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
{overrides.length > 0 ? (
|
||||||
|
<Tooltip
|
||||||
|
className={styles.tooltip}
|
||||||
|
label={
|
||||||
|
<div>
|
||||||
|
Here you can specify which users that <br />
|
||||||
|
should get this variant.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p style={{ marginBottom: '0' }}>
|
||||||
|
<strong>Overrides </strong>
|
||||||
|
<Icon name="info" />
|
||||||
|
</p>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
undefined
|
||||||
|
)}
|
||||||
|
|
||||||
|
<OverrideConfig
|
||||||
|
overrides={overrides}
|
||||||
|
removeOverrideOption={removeOverrideOption}
|
||||||
|
updateOverrideOption={updateOverrideOption}
|
||||||
|
/>
|
||||||
|
<a href="#add-override" onClick={onAddOverride}>
|
||||||
|
<small>Add override</small>
|
||||||
|
</a>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button type="button" raised colored type="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddVariant.propTypes = {
|
||||||
|
showDialog: PropTypes.bool.isRequired,
|
||||||
|
closeDialog: PropTypes.func.isRequired,
|
||||||
|
save: PropTypes.func.isRequired,
|
||||||
|
validateName: PropTypes.func.isRequired,
|
||||||
|
editVariant: PropTypes.object,
|
||||||
|
title: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddVariant;
|
47
frontend/src/component/feature/variant/override-config.jsx
Normal file
47
frontend/src/component/feature/variant/override-config.jsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Textfield, Grid, Cell, IconButton } from 'react-mdl';
|
||||||
|
import MySelect from '../form/select';
|
||||||
|
|
||||||
|
const overrideOptions = [
|
||||||
|
{ key: 'userId', label: 'userId' },
|
||||||
|
{ key: 'appName', label: 'appName' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function OverrideConfig({ overrides, updateOverrideOption, removeOverrideOption }) {
|
||||||
|
return overrides.map((o, i) => (
|
||||||
|
<Grid noSpacing key={`override=${i}`}>
|
||||||
|
<Cell col={3}>
|
||||||
|
<MySelect
|
||||||
|
name="contextName"
|
||||||
|
label="Context Field"
|
||||||
|
value={o.contextName}
|
||||||
|
options={overrideOptions}
|
||||||
|
onChange={updateOverrideOption(i)}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={8}>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Values"
|
||||||
|
name="values"
|
||||||
|
placeholder="val1, val2, ..."
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={o.values}
|
||||||
|
onChange={updateOverrideOption(i)}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={1} style={{ textAlign: 'right' }}>
|
||||||
|
<IconButton name="delete" onClick={removeOverrideOption(i)} />
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
OverrideConfig.propTypes = {
|
||||||
|
overrides: PropTypes.array.isRequired,
|
||||||
|
updateOverrideOption: PropTypes.func.isRequired,
|
||||||
|
removeOverrideOption: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OverrideConfig;
|
@ -1,108 +1,72 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { FormButtons } from '../../common';
|
|
||||||
import VariantViewComponent from './variant-view-component';
|
import VariantViewComponent from './variant-view-component';
|
||||||
import VariantEditComponent from './variant-edit-component';
|
|
||||||
import styles from './variant.scss';
|
import styles from './variant.scss';
|
||||||
import { UPDATE_FEATURE } from '../../../permissions';
|
import { UPDATE_FEATURE } from '../../../permissions';
|
||||||
|
import AddVariant from './add-variant';
|
||||||
|
|
||||||
|
const initalState = { showDialog: false, editVariant: undefined, editIndex: -1 };
|
||||||
|
|
||||||
class UpdateVariantComponent extends Component {
|
class UpdateVariantComponent extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.state = { ...initalState };
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
closeDialog = () => {
|
||||||
UNSAFE_componentWillMount() {
|
this.setState({ ...initalState });
|
||||||
// TODO unwind this stuff
|
};
|
||||||
if (this.props.initCallRequired === true) {
|
|
||||||
this.props.init(this.props.input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateWeight(newWeight, newSize) {
|
openAddVariant = e => {
|
||||||
const variants = this.props.input.variants || [];
|
e.preventDefault();
|
||||||
variants.forEach((v, i) => {
|
this.setState({
|
||||||
v.weight = newWeight;
|
showDialog: true,
|
||||||
this.props.updateVariant(i, v);
|
editVariant: undefined,
|
||||||
|
editIndex: undefined,
|
||||||
|
title: 'Add variant',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make sure the sum of weigths is 100.
|
|
||||||
const sum = newWeight * newSize;
|
|
||||||
if (sum !== 100) {
|
|
||||||
const first = variants[0];
|
|
||||||
first.weight = 100 - sum + first.weight;
|
|
||||||
this.props.updateVariant(0, first);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addVariant = (e, variants) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const size = variants.length + 1;
|
|
||||||
const percentage = parseInt((1 / size) * 100);
|
|
||||||
const variant = {
|
|
||||||
name: '',
|
|
||||||
weight: percentage,
|
|
||||||
edit: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateWeight(percentage, size);
|
openEditVariant = (e, index, variant) => {
|
||||||
this.props.addVariant(variant);
|
e.preventDefault();
|
||||||
|
if (this.props.hasPermission(UPDATE_FEATURE)) {
|
||||||
|
this.setState({
|
||||||
|
showDialog: true,
|
||||||
|
editVariant: variant,
|
||||||
|
editIndex: index,
|
||||||
|
title: 'Edit variant',
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
removeVariant = (e, index) => {
|
validateName = name => {
|
||||||
|
if (!name) {
|
||||||
|
return { name: 'Name is required' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveVariant = (e, index) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const variants = this.props.input.variants;
|
|
||||||
const size = variants.length - 1;
|
|
||||||
const percentage = parseInt((1 / size) * 100);
|
|
||||||
this.updateWeight(percentage, size);
|
|
||||||
this.props.removeVariant(index);
|
this.props.removeVariant(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
editVariant = (e, index, v) => {
|
renderVariant = (variant, index) => (
|
||||||
e.preventDefault();
|
|
||||||
if (this.props.hasPermission(UPDATE_FEATURE)) {
|
|
||||||
v.edit = true;
|
|
||||||
this.props.updateVariant(index, v);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
closeVariant = (e, index, v) => {
|
|
||||||
e.preventDefault();
|
|
||||||
v.edit = false;
|
|
||||||
this.props.updateVariant(index, v);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateVariant = (index, newVariant) => {
|
|
||||||
this.props.updateVariant(index, newVariant);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderVariant = (variant, index) =>
|
|
||||||
variant.edit ? (
|
|
||||||
<VariantEditComponent
|
|
||||||
key={index}
|
|
||||||
variant={variant}
|
|
||||||
removeVariant={e => this.removeVariant(e, index)}
|
|
||||||
closeVariant={e => this.closeVariant(e, index, variant)}
|
|
||||||
updateVariant={this.updateVariant.bind(this, index)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<VariantViewComponent
|
<VariantViewComponent
|
||||||
key={index}
|
key={index}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
editVariant={e => this.editVariant(e, index, variant)}
|
editVariant={e => this.openEditVariant(e, index, variant)}
|
||||||
removeVariant={e => this.removeVariant(e, index)}
|
removeVariant={e => this.onRemoveVariant(e, index)}
|
||||||
hasPermission={this.props.hasPermission}
|
hasPermission={this.props.hasPermission}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
renderVariants = variants => {
|
renderVariants = variants => (
|
||||||
if (variants.length > 0) {
|
|
||||||
return (
|
|
||||||
<table className={['mdl-data-table mdl-shadow--2dp', styles.variantTable].join(' ')}>
|
<table className={['mdl-data-table mdl-shadow--2dp', styles.variantTable].join(' ')}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Variant name</th>
|
||||||
|
<th className={styles.labels} />
|
||||||
<th>Weight</th>
|
<th>Weight</th>
|
||||||
<th className={styles.actions} />
|
<th className={styles.actions} />
|
||||||
</tr>
|
</tr>
|
||||||
@ -110,60 +74,47 @@ class UpdateVariantComponent extends Component {
|
|||||||
<tbody>{variants.map(this.renderVariant)}</tbody>
|
<tbody>{variants.map(this.renderVariant)}</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return <p />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onSubmit, onCancel, input, features } = this.props;
|
const { showDialog, editVariant, editIndex, title } = this.state;
|
||||||
const variants = input.variants || [];
|
const { variants, addVariant, updateVariant } = this.props;
|
||||||
|
const saveVariant = editVariant ? updateVariant.bind(null, editIndex) : addVariant;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ padding: '16px' }}>
|
<section style={{ padding: '16px', maxWidth: '700px' }}>
|
||||||
<p>
|
<p>
|
||||||
Variants allows you to return a variant object if the feature toggle is considered enabled for the
|
Variants allows you to return a variant object if the feature toggle is considered enabled for the
|
||||||
current request. When using variants you should use the{' '}
|
current request. When using variants you should use the{' '}
|
||||||
<code style={{ color: 'navy' }}>getVariant()</code> method in the client SDK.
|
<code style={{ color: 'navy' }}>getVariant()</code> method in the Client SDK.
|
||||||
</p>
|
|
||||||
<p style={{ backgroundColor: 'rgba(255, 229, 255, 0.4)', padding: '5px' }}>
|
|
||||||
The sum of variants weights needs to be a constant number to guarantee consistent hashing in the
|
|
||||||
client implementations, this is why we will sometime allocate a few more percentages to the first
|
|
||||||
variant if the sum is not exactly 100. In a final version of this feature it should be possible to
|
|
||||||
the user to manually set the percentages for each variant.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{this.renderVariants(variants)}
|
||||||
|
<br />
|
||||||
{this.props.hasPermission(UPDATE_FEATURE) ? (
|
{this.props.hasPermission(UPDATE_FEATURE) ? (
|
||||||
<p>
|
<p>
|
||||||
<a href="#add-variant" title="Add variant" onClick={e => this.addVariant(e, variants)}>
|
<a href="#add-variant" title="Add variant" onClick={this.openAddVariant}>
|
||||||
Add variant
|
Add variant
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
<AddVariant
|
||||||
<form onSubmit={onSubmit(input, features)}>
|
showDialog={showDialog}
|
||||||
{this.renderVariants(variants)}
|
closeDialog={this.closeDialog}
|
||||||
<br />
|
save={saveVariant}
|
||||||
{this.props.hasPermission(UPDATE_FEATURE) ? (
|
validateName={this.validateName}
|
||||||
<FormButtons submitText={'Save'} onCancel={onCancel} />
|
editVariant={editVariant}
|
||||||
) : null}
|
title={title}
|
||||||
</form>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateVariantComponent.propTypes = {
|
UpdateVariantComponent.propTypes = {
|
||||||
input: PropTypes.object,
|
variants: PropTypes.array.isRequired,
|
||||||
features: PropTypes.array,
|
|
||||||
setValue: PropTypes.func.isRequired,
|
|
||||||
addVariant: PropTypes.func.isRequired,
|
addVariant: PropTypes.func.isRequired,
|
||||||
removeVariant: PropTypes.func.isRequired,
|
removeVariant: PropTypes.func.isRequired,
|
||||||
updateVariant: PropTypes.func.isRequired,
|
updateVariant: PropTypes.func.isRequired,
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
onCancel: PropTypes.func.isRequired,
|
|
||||||
initCallRequired: PropTypes.bool,
|
|
||||||
init: PropTypes.func,
|
|
||||||
hasPermission: PropTypes.func.isRequired,
|
hasPermission: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,67 +1,36 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { requestUpdateFeatureToggleVariants } from '../../../store/feature-actions';
|
import { requestUpdateFeatureToggleVariants } from '../../../store/feature-actions';
|
||||||
import { createMapper, createActions } from '../../input-helpers';
|
|
||||||
import UpdateFeatureToggleComponent from './update-variant-component';
|
import UpdateFeatureToggleComponent from './update-variant-component';
|
||||||
|
import { updateWeight } from '../../common/util';
|
||||||
|
|
||||||
const ID = 'edit-toggle-variants';
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
function getId(props) {
|
variants: ownProps.featureToggle.variants || [],
|
||||||
return [ID, props.featureToggle.name];
|
hasPermission: ownProps.hasPermission,
|
||||||
}
|
});
|
||||||
// TODO: need to scope to the active featureToggle
|
|
||||||
// best is to emulate the "input-storage"?
|
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||||
const mapStateToProps = createMapper({
|
addVariant: variant => {
|
||||||
id: getId,
|
const { featureToggle } = ownProps;
|
||||||
getDefault: (state, ownProps) => ownProps.featureToggle,
|
const currentVariants = featureToggle.variants || [];
|
||||||
prepare: props => {
|
const variants = [...currentVariants, variant];
|
||||||
props.editmode = true;
|
updateWeight(variants, 1000);
|
||||||
return props;
|
return requestUpdateFeatureToggleVariants(featureToggle, variants)(dispatch);
|
||||||
|
},
|
||||||
|
removeVariant: index => {
|
||||||
|
const { featureToggle } = ownProps;
|
||||||
|
const currentVariants = featureToggle.variants || [];
|
||||||
|
const variants = currentVariants.filter((v, i) => i !== index);
|
||||||
|
updateWeight(variants, 1000);
|
||||||
|
requestUpdateFeatureToggleVariants(featureToggle, variants)(dispatch);
|
||||||
|
},
|
||||||
|
updateVariant: (index, variant) => {
|
||||||
|
const { featureToggle } = ownProps;
|
||||||
|
const currentVariants = featureToggle.variants || [];
|
||||||
|
const variants = currentVariants.map((v, i) => (i === index ? variant : v));
|
||||||
|
updateWeight(variants, 1000);
|
||||||
|
requestUpdateFeatureToggleVariants(featureToggle, variants)(dispatch);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const prepare = (methods, dispatch, ownProps) => {
|
export default connect(mapStateToProps, mapDispatchToProps)(UpdateFeatureToggleComponent);
|
||||||
methods.onSubmit = (input, features) => e => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const featureToggle = features.find(f => f.name === input.name);
|
|
||||||
|
|
||||||
// Kind of a hack
|
|
||||||
featureToggle.strategies.forEach(s => (s.id = undefined));
|
|
||||||
|
|
||||||
const variants = input.variants.map(v => {
|
|
||||||
delete v.edit;
|
|
||||||
return v;
|
|
||||||
});
|
|
||||||
|
|
||||||
requestUpdateFeatureToggleVariants(featureToggle, variants)(dispatch);
|
|
||||||
variants.forEach((v, i) => methods.updateInList('variants', i, v));
|
|
||||||
};
|
|
||||||
|
|
||||||
methods.onCancel = evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
ownProps.history.push(`/features/view/${ownProps.featureToggle.name}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
methods.addVariant = v => {
|
|
||||||
methods.pushToList('variants', v);
|
|
||||||
};
|
|
||||||
|
|
||||||
methods.removeVariant = index => {
|
|
||||||
methods.removeFromList('variants', index);
|
|
||||||
};
|
|
||||||
|
|
||||||
methods.updateVariant = (index, n) => {
|
|
||||||
methods.updateInList('variants', index, n);
|
|
||||||
};
|
|
||||||
|
|
||||||
methods.validateName = () => {};
|
|
||||||
|
|
||||||
return methods;
|
|
||||||
};
|
|
||||||
|
|
||||||
const actions = createActions({
|
|
||||||
id: getId,
|
|
||||||
prepare,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, actions)(UpdateFeatureToggleComponent);
|
|
||||||
|
@ -1,162 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { IconButton, Cell, Grid, Textfield, Tooltip, Icon } from 'react-mdl';
|
|
||||||
import styles from './variant.scss';
|
|
||||||
|
|
||||||
class VariantEditComponent extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.refs.name.inputRef.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserIdOverrides(variant) {
|
|
||||||
const overrides = variant.overrides || [];
|
|
||||||
const userIdOverrides = overrides.find(o => o.contextName === 'userId') || { values: [] };
|
|
||||||
return userIdOverrides.values.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleEditMode = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.setState({
|
|
||||||
editmode: !this.state.editmode,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
updateToggleName = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const variant = this.props.variant;
|
|
||||||
variant.name = e.target.value;
|
|
||||||
this.props.updateVariant(variant);
|
|
||||||
};
|
|
||||||
|
|
||||||
updatePayload = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const variant = this.props.variant;
|
|
||||||
variant.payload = {
|
|
||||||
type: 'string',
|
|
||||||
value: e.target.value,
|
|
||||||
};
|
|
||||||
this.props.updateVariant(variant);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateOverrides = (contextName, e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const values = e.target.value.split(',').map(v => v.trim());
|
|
||||||
const variant = this.props.variant;
|
|
||||||
|
|
||||||
// Clean empty string. (should be moved to action)
|
|
||||||
if (values.length === 1 && !values[0]) {
|
|
||||||
variant.overrides = undefined;
|
|
||||||
} else {
|
|
||||||
variant.overrides = [{ contextName, values }];
|
|
||||||
}
|
|
||||||
this.props.updateVariant(variant);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { variant, closeVariant, removeVariant } = this.props;
|
|
||||||
const payload = variant.payload ? variant.payload.value : '';
|
|
||||||
const userIdOverrides = this.getUserIdOverrides(variant);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<Grid noSpacing>
|
|
||||||
<Cell col={6}>
|
|
||||||
<Textfield
|
|
||||||
floatingLabel
|
|
||||||
ref="name"
|
|
||||||
label="Name"
|
|
||||||
name="name"
|
|
||||||
required
|
|
||||||
value={variant.name}
|
|
||||||
onChange={this.updateToggleName}
|
|
||||||
/>
|
|
||||||
</Cell>
|
|
||||||
<Cell col={6} />
|
|
||||||
</Grid>
|
|
||||||
<Grid noSpacing>
|
|
||||||
<Cell col={11}>
|
|
||||||
<Textfield
|
|
||||||
floatingLabel
|
|
||||||
rows={1}
|
|
||||||
label="Payload"
|
|
||||||
name="payload"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
value={payload}
|
|
||||||
onChange={this.updatePayload}
|
|
||||||
/>
|
|
||||||
</Cell>
|
|
||||||
<Cell col={1} style={{ margin: 'auto', padding: '0 5px' }}>
|
|
||||||
<Tooltip
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Passed to the variant object. <br />
|
|
||||||
Can be anything (json, value, csv)
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon name="info" />
|
|
||||||
</Tooltip>
|
|
||||||
</Cell>
|
|
||||||
</Grid>
|
|
||||||
<Grid noSpacing>
|
|
||||||
<Cell col={11}>
|
|
||||||
<Textfield
|
|
||||||
floatingLabel
|
|
||||||
label="overrides.userId"
|
|
||||||
name="overrides.userId"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
value={userIdOverrides}
|
|
||||||
onChange={this.updateOverrides.bind(this, 'userId')}
|
|
||||||
/>
|
|
||||||
</Cell>
|
|
||||||
<Cell col={1} style={{ margin: 'auto', padding: '0 5px' }}>
|
|
||||||
<Tooltip
|
|
||||||
label={
|
|
||||||
<div>
|
|
||||||
Here you can specify which users that <br />
|
|
||||||
should get this variant.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon name="info" />
|
|
||||||
</Tooltip>
|
|
||||||
</Cell>
|
|
||||||
</Grid>
|
|
||||||
<a href="#close" onClick={closeVariant}>
|
|
||||||
Close
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td className={styles.actions}>
|
|
||||||
<Textfield
|
|
||||||
floatingLabel
|
|
||||||
label="Weight"
|
|
||||||
name="weight"
|
|
||||||
value={variant.weight}
|
|
||||||
style={{ width: '40px' }}
|
|
||||||
disabled
|
|
||||||
onChange={() => {}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className={styles.actions}>
|
|
||||||
<IconButton name="expand_less" onClick={closeVariant} />
|
|
||||||
<IconButton name="delete" onClick={removeVariant} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VariantEditComponent.propTypes = {
|
|
||||||
variant: PropTypes.object,
|
|
||||||
removeVariant: PropTypes.func,
|
|
||||||
updateVariant: PropTypes.func,
|
|
||||||
closeVariant: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VariantEditComponent;
|
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { IconButton } from 'react-mdl';
|
import { IconButton, Chip } from 'react-mdl';
|
||||||
import styles from './variant.scss';
|
import styles from './variant.scss';
|
||||||
import { UPDATE_FEATURE } from '../../../permissions';
|
import { UPDATE_FEATURE } from '../../../permissions';
|
||||||
|
|
||||||
@ -9,10 +9,18 @@ function VariantViewComponent({ variant, editVariant, removeVariant, hasPermissi
|
|||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td onClick={editVariant}>{variant.name}</td>
|
<td onClick={editVariant}>{variant.name}</td>
|
||||||
<td>{variant.weight}</td>
|
<td className={styles.labels}>
|
||||||
|
{variant.payload ? <Chip>Payload</Chip> : undefined}{' '}
|
||||||
|
{variant.overrides && variant.overrides.length > 0 ? (
|
||||||
|
<Chip style={{ backgroundColor: 'rgba(173, 216, 230, 0.2)' }}>Overrides</Chip>
|
||||||
|
) : (
|
||||||
|
undefined
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>{variant.weight / 10.0} %</td>
|
||||||
{hasPermission(UPDATE_FEATURE) ? (
|
{hasPermission(UPDATE_FEATURE) ? (
|
||||||
<td className={styles.actions}>
|
<td className={styles.actions}>
|
||||||
<IconButton name="expand_more" onClick={editVariant} />
|
<IconButton name="edit" onClick={editVariant} />
|
||||||
<IconButton name="delete" onClick={removeVariant} />
|
<IconButton name="delete" onClick={removeVariant} />
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
.variantTable {
|
.variantTable {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 700px;
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -14,6 +15,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
th.labels {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
td.labels {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th.labels {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.labels {
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
th.actions {
|
th.actions {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@ -22,3 +41,21 @@ td.actions {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
max-width: 90%;
|
||||||
|
width: 600px;
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.modal {
|
||||||
|
top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user