1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

fix: improve on variant ui

This commit is contained in:
Ivar Conradi Østhus 2020-05-20 16:32:29 +02:00
parent 9c009627ba
commit 5cbfcf5f3b
11 changed files with 1101 additions and 558 deletions

View File

@ -29,4 +29,9 @@ module.exports = {
FooterDropDownSection: 'react-mdl-FooterDropDownSection',
FooterSection: 'react-mdl-FooterSection',
FooterLinkList: 'react-mdl-FooterLinkList',
Tooltip: 'react-mdl-Tooltip',
Dialog: 'react-mdl-Dialog',
DialogTitle: 'react-mdl-DialogTitle',
DialogContent: 'react-mdl-DialogContent',
DialogActions: 'react-mdl-DialogActions',
};

View File

@ -20,3 +20,13 @@ export const trim = 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;
}

View File

@ -4,6 +4,7 @@ exports[`renders correctly with with variants 1`] = `
<section
style={
Object {
"maxWidth": "700px",
"padding": "16px",
}
}
@ -20,18 +21,127 @@ exports[`renders correctly with with variants 1`] = `
>
getVariant()
</code>
method in the client SDK.
method in the Client SDK.
</p>
<p
style={
Object {
"backgroundColor": "rgba(255, 229, 255, 0.4)",
"padding": "5px",
}
}
<table
className="mdl-data-table mdl-shadow--2dp variantTable"
>
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>
<thead>
<tr>
<th>
Variant name
</th>
<th
className="labels"
/>
<th>
Weight
</th>
<th
className="actions"
/>
</tr>
</thead>
<tbody>
<tr>
<td
onClick={[Function]}
>
blue
</td>
<td
className="labels"
>
<react-mdl-Chip
style={
Object {
"backgroundColor": "rgba(173, 216, 230, 0.2)",
}
}
>
Overrides
</react-mdl-Chip>
</td>
<td>
3.4
%
</td>
<td
className="actions"
>
<react-mdl-IconButton
name="edit"
onClick={[Function]}
/>
<react-mdl-IconButton
name="delete"
onClick={[Function]}
/>
</td>
</tr>
<tr>
<td
onClick={[Function]}
>
yellow
</td>
<td
className="labels"
>
</td>
<td>
3.3
%
</td>
<td
className="actions"
>
<react-mdl-IconButton
name="edit"
onClick={[Function]}
/>
<react-mdl-IconButton
name="delete"
onClick={[Function]}
/>
</td>
</tr>
<tr>
<td
onClick={[Function]}
>
orange
</td>
<td
className="labels"
>
<react-mdl-Chip>
Payload
</react-mdl-Chip>
</td>
<td>
3.3
%
</td>
<td
className="actions"
>
<react-mdl-IconButton
name="edit"
onClick={[Function]}
/>
<react-mdl-IconButton
name="delete"
onClick={[Function]}
/>
</td>
</tr>
</tbody>
</table>
<br />
<p>
<a
href="#add-variant"
@ -41,115 +151,167 @@ exports[`renders correctly with with variants 1`] = `
Add variant
</a>
</p>
<form>
<table
className="mdl-data-table mdl-shadow--2dp variantTable"
<form
onSubmit={[Function]}
>
<react-mdl-Dialog
className="modal"
open={false}
>
<thead>
<tr>
<th>
Name
</th>
<th>
Weight
</th>
<th
className="actions"
/>
</tr>
</thead>
<tbody>
<tr>
<td
onClick={[Function]}
>
blue
</td>
<td>
34
</td>
<td
className="actions"
>
<react-mdl-IconButton
name="expand_more"
onClick={[Function]}
/>
<react-mdl-IconButton
name="delete"
onClick={[Function]}
/>
</td>
</tr>
<tr>
<td
onClick={[Function]}
>
yellow
</td>
<td>
33
</td>
<td
className="actions"
>
<react-mdl-IconButton
name="expand_more"
onClick={[Function]}
/>
<react-mdl-IconButton
name="delete"
onClick={[Function]}
/>
</td>
</tr>
<tr>
<td
onClick={[Function]}
>
orange
</td>
<td>
33
</td>
<td
className="actions"
>
<react-mdl-IconButton
name="expand_more"
onClick={[Function]}
/>
<react-mdl-IconButton
name="delete"
onClick={[Function]}
/>
</td>
</tr>
</tbody>
</table>
<br />
<div>
<react-mdl-Button
icon="add"
primary={true}
raised={true}
ripple={true}
type="submit"
<react-mdl-DialogTitle
style={
Object {
"padding": "10px",
}
}
/>
<react-mdl-DialogContent
style={
Object {
"minHeight": "350px",
"padding": "10px",
}
}
>
<react-mdl-Icon
name="add"
<p
style={
Object {
"color": "red",
}
}
/>
   
Save
</react-mdl-Button>
 
<react-mdl-Button
onClick={[MockFunction]}
type="cancel"
>
Cancel
</react-mdl-Button>
</div>
<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>
</section>
`;
@ -158,6 +320,7 @@ exports[`renders correctly with without variants 1`] = `
<section
style={
Object {
"maxWidth": "700px",
"padding": "16px",
}
}
@ -174,18 +337,30 @@ exports[`renders correctly with without variants 1`] = `
>
getVariant()
</code>
method in the client SDK.
method in the Client SDK.
</p>
<p
style={
Object {
"backgroundColor": "rgba(255, 229, 255, 0.4)",
"padding": "5px",
}
}
<table
className="mdl-data-table mdl-shadow--2dp variantTable"
>
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>
<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"
@ -195,31 +370,167 @@ exports[`renders correctly with without variants 1`] = `
Add variant
</a>
</p>
<form>
<p />
<br />
<div>
<react-mdl-Button
icon="add"
primary={true}
raised={true}
ripple={true}
type="submit"
<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",
}
}
>
<react-mdl-Icon
name="add"
<p
style={
Object {
"color": "red",
}
}
/>
   
Save
</react-mdl-Button>
 
<react-mdl-Button
onClick={[MockFunction]}
type="cancel"
>
Cancel
</react-mdl-Button>
</div>
<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>
</section>
`;
@ -228,6 +539,7 @@ exports[`renders correctly with without variants and no permissions 1`] = `
<section
style={
Object {
"maxWidth": "700px",
"padding": "16px",
}
}
@ -244,21 +556,191 @@ exports[`renders correctly with without variants and no permissions 1`] = `
>
getVariant()
</code>
method in the client SDK.
method in the Client SDK.
</p>
<p
style={
Object {
"backgroundColor": "rgba(255, 229, 255, 0.4)",
"padding": "5px",
}
}
<table
className="mdl-data-table mdl-shadow--2dp variantTable"
>
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>
<form>
<p />
<br />
<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>
</section>
`;

View File

@ -8,34 +8,14 @@ import { UPDATE_FEATURE } from '../../../../permissions';
jest.mock('react-mdl');
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(
<MemoryRouter>
<UpdateVariant
key={0}
input={featureToggle}
onCancel={jest.fn()}
features={[]}
setValue={jest.fn()}
variants={[]}
addVariant={jest.fn()}
removeVariant={jest.fn()}
updateVariant={jest.fn()}
onSubmit={jest.fn()}
onCancel={jest.fn()}
init={jest.fn()}
hasPermission={permission => permission === UPDATE_FEATURE}
/>
</MemoryRouter>
@ -45,34 +25,14 @@ test('renders correctly with without variants', () => {
});
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(
<MemoryRouter>
<UpdateVariant
key={0}
input={featureToggle}
onCancel={jest.fn()}
features={[]}
setValue={jest.fn()}
variants={[]}
addVariant={jest.fn()}
removeVariant={jest.fn()}
updateVariant={jest.fn()}
onSubmit={jest.fn()}
onCancel={jest.fn()}
init={jest.fn()}
hasPermission={() => false}
/>
</MemoryRouter>
@ -124,16 +84,10 @@ test('renders correctly with with variants', () => {
<MemoryRouter>
<UpdateVariant
key={0}
input={featureToggle}
onCancel={jest.fn()}
features={[]}
setValue={jest.fn()}
variants={featureToggle.variants}
addVariant={jest.fn()}
removeVariant={jest.fn()}
updateVariant={jest.fn()}
onSubmit={jest.fn()}
onCancel={jest.fn()}
init={jest.fn()}
hasPermission={permission => permission === UPDATE_FEATURE}
/>
</MemoryRouter>

View 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;

View 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;

View File

@ -1,169 +1,120 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormButtons } from '../../common';
import VariantViewComponent from './variant-view-component';
import VariantEditComponent from './variant-edit-component';
import styles from './variant.scss';
import { UPDATE_FEATURE } from '../../../permissions';
import AddVariant from './add-variant';
const initalState = { showDialog: false, editVariant: undefined, editIndex: -1 };
class UpdateVariantComponent extends Component {
constructor(props) {
super(props);
this.state = { ...initalState };
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
// TODO unwind this stuff
if (this.props.initCallRequired === true) {
this.props.init(this.props.input);
}
}
updateWeight(newWeight, newSize) {
const variants = this.props.input.variants || [];
variants.forEach((v, i) => {
v.weight = newWeight;
this.props.updateVariant(i, v);
});
// 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);
this.props.addVariant(variant);
closeDialog = () => {
this.setState({ ...initalState });
};
removeVariant = (e, index) => {
openAddVariant = e => {
e.preventDefault();
this.setState({
showDialog: true,
editVariant: undefined,
editIndex: undefined,
title: 'Add variant',
});
};
openEditVariant = (e, index, variant) => {
e.preventDefault();
if (this.props.hasPermission(UPDATE_FEATURE)) {
this.setState({
showDialog: true,
editVariant: variant,
editIndex: index,
title: 'Edit variant',
});
}
};
validateName = name => {
if (!name) {
return { name: 'Name is required' };
}
};
onRemoveVariant = (e, index) => {
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);
};
editVariant = (e, index, v) => {
e.preventDefault();
if (this.props.hasPermission(UPDATE_FEATURE)) {
v.edit = true;
this.props.updateVariant(index, v);
}
};
renderVariant = (variant, index) => (
<VariantViewComponent
key={index}
variant={variant}
editVariant={e => this.openEditVariant(e, index, variant)}
removeVariant={e => this.onRemoveVariant(e, index)}
hasPermission={this.props.hasPermission}
/>
);
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
key={index}
variant={variant}
editVariant={e => this.editVariant(e, index, variant)}
removeVariant={e => this.removeVariant(e, index)}
hasPermission={this.props.hasPermission}
/>
);
renderVariants = variants => {
if (variants.length > 0) {
return (
<table className={['mdl-data-table mdl-shadow--2dp', styles.variantTable].join(' ')}>
<thead>
<tr>
<th>Name</th>
<th>Weight</th>
<th className={styles.actions} />
</tr>
</thead>
<tbody>{variants.map(this.renderVariant)}</tbody>
</table>
);
} else {
return <p />;
}
};
renderVariants = variants => (
<table className={['mdl-data-table mdl-shadow--2dp', styles.variantTable].join(' ')}>
<thead>
<tr>
<th>Variant name</th>
<th className={styles.labels} />
<th>Weight</th>
<th className={styles.actions} />
</tr>
</thead>
<tbody>{variants.map(this.renderVariant)}</tbody>
</table>
);
render() {
const { onSubmit, onCancel, input, features } = this.props;
const variants = input.variants || [];
const { showDialog, editVariant, editIndex, title } = this.state;
const { variants, addVariant, updateVariant } = this.props;
const saveVariant = editVariant ? updateVariant.bind(null, editIndex) : addVariant;
return (
<section style={{ padding: '16px' }}>
<section style={{ padding: '16px', maxWidth: '700px' }}>
<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={{ 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.
<code style={{ color: 'navy' }}>getVariant()</code> method in the Client SDK.
</p>
{this.renderVariants(variants)}
<br />
{this.props.hasPermission(UPDATE_FEATURE) ? (
<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
</a>
</p>
) : null}
<form onSubmit={onSubmit(input, features)}>
{this.renderVariants(variants)}
<br />
{this.props.hasPermission(UPDATE_FEATURE) ? (
<FormButtons submitText={'Save'} onCancel={onCancel} />
) : null}
</form>
<AddVariant
showDialog={showDialog}
closeDialog={this.closeDialog}
save={saveVariant}
validateName={this.validateName}
editVariant={editVariant}
title={title}
/>
</section>
);
}
}
UpdateVariantComponent.propTypes = {
input: PropTypes.object,
features: PropTypes.array,
setValue: PropTypes.func.isRequired,
variants: PropTypes.array.isRequired,
addVariant: PropTypes.func.isRequired,
removeVariant: 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,
};

View File

@ -1,67 +1,36 @@
import { connect } from 'react-redux';
import { requestUpdateFeatureToggleVariants } from '../../../store/feature-actions';
import { createMapper, createActions } from '../../input-helpers';
import UpdateFeatureToggleComponent from './update-variant-component';
import { updateWeight } from '../../common/util';
const ID = 'edit-toggle-variants';
function getId(props) {
return [ID, props.featureToggle.name];
}
// TODO: need to scope to the active featureToggle
// best is to emulate the "input-storage"?
const mapStateToProps = createMapper({
id: getId,
getDefault: (state, ownProps) => ownProps.featureToggle,
prepare: props => {
props.editmode = true;
return props;
const mapStateToProps = (state, ownProps) => ({
variants: ownProps.featureToggle.variants || [],
hasPermission: ownProps.hasPermission,
});
const mapDispatchToProps = (dispatch, ownProps) => ({
addVariant: variant => {
const { featureToggle } = ownProps;
const currentVariants = featureToggle.variants || [];
const variants = [...currentVariants, variant];
updateWeight(variants, 1000);
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) => {
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);
export default connect(mapStateToProps, mapDispatchToProps)(UpdateFeatureToggleComponent);

View File

@ -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;

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IconButton } from 'react-mdl';
import { IconButton, Chip } from 'react-mdl';
import styles from './variant.scss';
import { UPDATE_FEATURE } from '../../../permissions';
@ -9,10 +9,18 @@ function VariantViewComponent({ variant, editVariant, removeVariant, hasPermissi
return (
<tr>
<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) ? (
<td className={styles.actions}>
<IconButton name="expand_more" onClick={editVariant} />
<IconButton name="edit" onClick={editVariant} />
<IconButton name="delete" onClick={removeVariant} />
</td>
) : (

View File

@ -1,5 +1,6 @@
.variantTable {
width: 100%;
max-width: 700px;
th, td {
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 {
text-align: right;
}
@ -22,3 +41,21 @@ td.actions {
text-align: right;
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;
}
}