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

feat: Addon support from UI (#236)

This commit is contained in:
Ivar Conradi Østhus 2021-02-05 14:24:22 +01:00 committed by GitHub
parent f7f58fa0a7
commit 3a6fa577bc
32 changed files with 1394 additions and 34 deletions

View File

@ -63,7 +63,7 @@
"eslint-config-finn-prettier": "^3.0.2",
"eslint-config-finn-react": "^2.0.2",
"eslint-plugin-react": "^7.11.1",
"fetch-mock": "^9.4.0",
"fetch-mock": "^9.11.0",
"identity-obj-proxy": "^3.0.0",
"immutable": "^3.8.1",
"jest": "^24.9.0",
@ -112,6 +112,7 @@
],
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
],
"testPathIgnorePatterns": ["/src/store/addons/__tests__/data.js"]
}
}

1
frontend/public/jira.svg Normal file
View File

@ -0,0 +1 @@
<svg height="2500" preserveAspectRatio="xMidYMid" width="2500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 -30.632388516510233 255.324 285.95638851651023"><linearGradient id="a"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient><linearGradient id="b" x1="98.031%" x2="58.888%" xlink:href="#a" y1=".161%" y2="40.766%"/><linearGradient id="c" x1="100.665%" x2="55.402%" xlink:href="#a" y1=".455%" y2="44.727%"/><path d="M244.658 0H121.707a55.502 55.502 0 0 0 55.502 55.502h22.649V77.37c.02 30.625 24.841 55.447 55.466 55.467V10.666C255.324 4.777 250.55 0 244.658 0z" fill="#2684ff"/><path d="M183.822 61.262H60.872c.019 30.625 24.84 55.447 55.466 55.467h22.649v21.938c.039 30.625 24.877 55.43 55.502 55.43V71.93c0-5.891-4.776-10.667-10.667-10.667z" fill="url(#b)"/><path d="M122.951 122.489H0c0 30.653 24.85 55.502 55.502 55.502h22.72v21.867c.02 30.597 24.798 55.408 55.396 55.466V133.156c0-5.891-4.776-10.667-10.667-10.667z" fill="url(#c)"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg enable-background="new 0 0 2447.6 2452.5" viewBox="0 0 2447.6 2452.5" xmlns="http://www.w3.org/2000/svg"><g clip-rule="evenodd" fill-rule="evenodd"><path d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z" fill="#36c5f0"/><path d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z" fill="#2eb67d"/><path d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z" fill="#ecb22e"/><path d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0" fill="#e01e5a"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg width="2500" height="2334" viewBox="0 0 256 239" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M119.54 100.503c-10.61 17.836-20.775 35.108-31.152 52.25-2.665 4.401-3.984 7.986-1.855 13.58 5.878 15.454-2.414 30.493-17.998 34.575-14.697 3.851-29.016-5.808-31.932-21.543-2.584-13.927 8.224-27.58 23.58-29.757 1.286-.184 2.6-.205 4.762-.367l23.358-39.168C73.612 95.465 64.868 78.39 66.803 57.23c1.368-14.957 7.25-27.883 18-38.477 20.59-20.288 52.002-23.573 76.246-8.001 23.284 14.958 33.948 44.094 24.858 69.031-6.854-1.858-13.756-3.732-21.343-5.79 2.854-13.865.743-26.315-8.608-36.981-6.178-7.042-14.106-10.733-23.12-12.093-18.072-2.73-35.815 8.88-41.08 26.618-5.976 20.13 3.069 36.575 27.784 48.967z" fill="#C73A63"/><path d="M149.841 79.41c7.475 13.187 15.065 26.573 22.587 39.836 38.02-11.763 66.686 9.284 76.97 31.817 12.422 27.219 3.93 59.457-20.465 76.25-25.04 17.238-56.707 14.293-78.892-7.851 5.654-4.733 11.336-9.487 17.407-14.566 21.912 14.192 41.077 13.524 55.305-3.282 12.133-14.337 11.87-35.714-.615-49.75-14.408-16.197-33.707-16.691-57.035-1.143-9.677-17.168-19.522-34.199-28.893-51.491-3.16-5.828-6.648-9.21-13.77-10.443-11.893-2.062-19.571-12.275-20.032-23.717-.453-11.316 6.214-21.545 16.634-25.53 10.322-3.949 22.435-.762 29.378 8.014 5.674 7.17 7.477 15.24 4.491 24.083-.83 2.466-1.905 4.852-3.07 7.774z" fill="#4B4B4B"/><path d="M167.707 187.21h-45.77c-4.387 18.044-13.863 32.612-30.19 41.876-12.693 7.2-26.373 9.641-40.933 7.29-26.808-4.323-48.728-28.456-50.658-55.63-2.184-30.784 18.975-58.147 47.178-64.293 1.947 7.071 3.915 14.21 5.862 21.264-25.876 13.202-34.832 29.836-27.59 50.636 6.375 18.304 24.484 28.337 44.147 24.457 20.08-3.962 30.204-20.65 28.968-47.432 19.036 0 38.088-.197 57.126.097 7.434.117 13.173-.654 18.773-7.208 9.22-10.784 26.191-9.811 36.121.374 10.148 10.409 9.662 27.157-1.077 37.127-10.361 9.62-26.73 9.106-36.424-1.26-1.992-2.136-3.562-4.673-5.533-7.298z" fill="#4A4A4A"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,177 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Textfield, Card, CardTitle, CardText, CardActions, Switch, Grid, Cell } from 'react-mdl';
import { FormButtons, styles as commonStyles } from '../common';
import { trim } from '../common/util';
import AddonParameters from './form-addon-parameters';
import AddonEvents from './form-addon-events';
import { cloneDeep } from 'lodash';
const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }) => {
const [config, setConfig] = useState(addon);
const [errors, setErrors] = useState({
parameters: {},
});
const submitText = editMode ? 'Update' : 'Create';
useEffect(() => {
if (!provider) {
fetch();
}
}, []); // empty array => fetch only first time
useEffect(() => {
setConfig({ ...addon });
}, [addon.id]);
useEffect(() => {
if (provider && !config.provider) {
setConfig({ ...addon, provider: provider.name });
}
}, [provider]);
const setFieldValue = field => evt => {
evt.preventDefault();
const newConfig = { ...config };
newConfig[field] = evt.target.value;
setConfig(newConfig);
};
const onEnabled = evt => {
evt.preventDefault();
const enabled = !config.enabled;
setConfig({ ...config, enabled });
};
const setParameterValue = param => evt => {
evt.preventDefault();
const newConfig = { ...config };
newConfig.parameters[param] = evt.target.value;
setConfig(newConfig);
};
const setEventValue = name => evt => {
const newConfig = { ...config };
if (evt.target.checked) {
newConfig.events.push(name);
} else {
newConfig.events = newConfig.events.filter(e => e !== name);
}
setConfig(newConfig);
setErrors({ ...errors, events: undefined });
};
const onSubmit = async evt => {
evt.preventDefault();
if (!provider) return;
const updatedErrors = cloneDeep(errors);
updatedErrors.containsErrors = false;
// Validations
if (config.events.length === 0) {
updatedErrors.events = 'You must listen to at least one event';
updatedErrors.containsErrors = true;
}
provider.parameters.forEach(p => {
const value = trim(config.parameters[p.name]);
if (p.required && !value) {
updatedErrors.parameters[p.name] = 'This field is required';
updatedErrors.containsErrors = true;
}
});
if (updatedErrors.containsErrors) {
setErrors(updatedErrors);
return;
}
try {
await submit(config);
} catch (e) {
setErrors({ parameters: {}, general: e.message });
}
};
const { name, description, documentationUrl = 'https://unleash.github.io/docs/addons' } = provider ? provider : {};
return (
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<CardTitle style={{ paddingTop: '24px', paddingBottom: '0', wordBreak: 'break-all' }}>
Configure {name}
</CardTitle>
<CardText>
{description}&nbsp;
<a href={documentationUrl} target="_blank">
Read more
</a>
<p className={commonStyles.error}>{errors.general}</p>
</CardText>
<form onSubmit={onSubmit}>
<section style={{ padding: '16px' }}>
<Grid noSpacing>
<Cell col={4}>
<Textfield
floatingLabel
label="Provider"
name="provider"
value={config.provider}
disabled
/>
</Cell>
<Cell col={4} style={{ paddingTop: '14px' }}>
<Switch checked={config.enabled} onChange={onEnabled}>
{config.enabled ? 'Enabled' : 'Disabled'}
</Switch>
</Cell>
</Grid>
<Textfield
floatingLabel
style={{ width: '80%' }}
rows={1}
label="Description"
name="description"
placeholder=""
value={config.description}
error={errors.description}
onChange={setFieldValue('description')}
/>
</section>
<section style={{ padding: '16px' }}>
<AddonEvents
provider={provider}
checkedEvents={config.events}
setEventValue={setEventValue}
error={errors.events}
/>
</section>
<section style={{ padding: '16px' }}>
<AddonParameters
provider={provider}
config={config}
errors={errors}
editMode={editMode}
setParameterValue={setParameterValue}
/>
</section>
<CardActions>
<FormButtons submitText={submitText} onCancel={cancel} />
</CardActions>
</form>
</Card>
);
};
AddonFormComponent.propTypes = {
provider: PropTypes.object,
addon: PropTypes.object.isRequired,
fetch: PropTypes.func.isRequired,
submit: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
editMode: PropTypes.bool.isRequired,
};
export default AddonFormComponent;

View File

@ -0,0 +1,55 @@
import { connect } from 'react-redux';
import FormComponent from './form-addon-component';
import { updateAddon, createAddon, fetchAddons } from '../../store/addons/actions';
import { cloneDeep } from 'lodash';
// Required for to fill the inital form.
const DEFAULT_DATA = {
provider: '',
description: '',
enabled: true,
parameters: {},
events: [],
};
const mapStateToProps = (state, params) => {
const defaultAddon = cloneDeep(DEFAULT_DATA);
const editMode = !!params.addonId;
const addons = state.addons.get('addons').toJS();
const providers = state.addons.get('providers').toJS();
let addon;
let provider;
if (editMode) {
addon = addons.find(addon => addon.id === +params.addonId) || defaultAddon;
provider = addon ? providers.find(provider => provider.name === addon.provider) : undefined;
} else {
provider = providers.find(provider => provider.name === params.provider);
addon = { ...defaultAddon, provider: provider ? provider.name : '' };
}
return {
provider,
addon,
editMode,
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
const { addonId, history } = ownProps;
const submit = addonId ? updateAddon : createAddon;
return {
submit: async addonConfig => {
await submit(addonConfig)(dispatch);
history.push('/addons');
},
fetch: () => fetchAddons()(dispatch),
cancel: () => history.push('/addons'),
};
};
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(FormComponent);
export default FormAddContainer;

View File

@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Checkbox, Grid, Cell } from 'react-mdl';
import { styles as commonStyles } from '../common';
const AddonEvents = ({ provider, checkedEvents, setEventValue, error }) => {
if (!provider) return null;
return (
<React.Fragment>
<h4>Events</h4>
<span className={commonStyles.error}>{error}</span>
<Grid className="demo-grid-ruler">
{provider.events.map(e => (
<Cell col={4} key={e}>
<Checkbox label={e} ripple checked={checkedEvents.includes(e)} onChange={setEventValue(e)} />
</Cell>
))}
</Grid>
</React.Fragment>
);
};
AddonEvents.propTypes = {
provider: PropTypes.object,
checkedEvents: PropTypes.array.isRequired,
setEventValue: PropTypes.func.isRequired,
error: PropTypes.string,
};
export default AddonEvents;

View File

@ -0,0 +1,82 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Textfield } from 'react-mdl';
const MASKED_VALUE = '*****';
const resolveType = ({ type = 'text', sensitive = false }, value) => {
if (sensitive && value === MASKED_VALUE) {
return 'text';
}
if (type === 'textfield') {
return 'text';
}
return type;
};
const AddonParameter = ({ definition, config, errors, setParameterValue }) => {
const value = config.parameters[definition.name] || '';
const type = resolveType(definition, value);
const error = errors.parameters[definition.name];
const descStyle = { fontSize: '0.8em', color: 'gray', marginTop: error ? '2px' : '-15px' };
return (
<div style={{ width: '80%', marginTop: '25px' }}>
<Textfield
floatingLabel
style={{ width: '100%' }}
rows={definition.type === 'textfield' ? 9 : 0}
type={type}
label={definition.displayName}
name={definition.name}
placeholder={definition.placeholder || ''}
value={value}
error={error}
onChange={setParameterValue(definition.name)}
/>
<div style={descStyle}>{definition.description}</div>
</div>
);
};
AddonParameter.propTypes = {
definition: PropTypes.object.isRequired,
config: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
setParameterValue: PropTypes.func.isRequired,
};
const AddonParameters = ({ provider, config, errors, setParameterValue, editMode }) => {
if (!provider) return null;
return (
<React.Fragment>
<h4>Parameters</h4>
{editMode ? (
<p>
Sensitive parameters will be masked with value "<i>*****</i>". If you don't change the value they
will not be updated when saving.
</p>
) : null}
{provider.parameters.map(p => (
<AddonParameter
key={p.name}
definition={p}
errors={errors}
config={config}
setParameterValue={setParameterValue}
/>
))}
</React.Fragment>
);
};
AddonParameters.propTypes = {
provider: PropTypes.object,
config: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
setParameterValue: PropTypes.func.isRequired,
editMode: PropTypes.bool.optional,
};
export default AddonParameters;

View File

@ -0,0 +1,117 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { List, ListItem, ListItemAction, IconButton, Card, Button } from 'react-mdl';
import { HeaderTitle, styles as commonStyles } from '../common';
import { CREATE_ADDON, DELETE_ADDON, UPDATE_ADDON } from '../../permissions';
const style = { width: '40px', height: '40px', marginRight: '16px', float: 'left' };
const getIcon = name => {
switch (name) {
case 'slack':
return <img style={style} src="public/slack.svg" />;
case 'jira-comment':
return <img style={style} src="public/jira.svg" />;
case 'webhook':
return <img style={style} src="public/webhooks.svg" />;
default:
return <i className="material-icons mdl-list__item-avatar">device_hub</i>;
}
};
const AddonListComponent = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history, hasPermission }) => {
useEffect(() => {
if (addons.length === 0) {
fetchAddons();
}
}, []);
const onRemoveAddon = addon => () => removeAddon(addon);
return (
<div>
{addons.length > 0 ? (
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<HeaderTitle title="Configured addons" />
<List>
{addons.map(addon => (
<ListItem key={addon.id} threeLine>
<span className={['mdl-list__item-primary-content'].join(' ')}>
{getIcon(addon.provider)}
<span>
{hasPermission(UPDATE_ADDON) ? (
<Link to={`/addons/edit/${addon.id}`}>
<strong>{addon.provider}</strong>
</Link>
) : (
<strong>{addon.provider}</strong>
)}
{addon.enabled ? null : <small> (Disabled)</small>}
</span>
<span className="mdl-list__item-text-body">{addon.description}</span>
</span>
<ListItemAction>
{hasPermission(UPDATE_ADDON) ? (
<IconButton
name={addon.enabled ? 'visibility' : 'visibility_off'}
title={addon.enabled ? 'Disable addon' : 'Enable addon'}
onClick={() => toggleAddon(addon)}
/>
) : null}
{hasPermission(DELETE_ADDON) ? (
<IconButton name="delete" title="Remove addon" onClick={onRemoveAddon(addon)} />
) : null}
</ListItemAction>
</ListItem>
))}
</List>
</Card>
) : null}
<br />
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<HeaderTitle title="Available addons" />
<List>
{providers.map((provider, i) => (
<ListItem key={i} threeLine>
<span className={['mdl-list__item-primary-content'].join(' ')}>
{getIcon(provider.name)}
<span>
<strong>{provider.displayName}</strong>&nbsp;
</span>
<span className="mdl-list__item-text-body">{provider.description}</span>
</span>
<ListItemAction>
{hasPermission(CREATE_ADDON) ? (
<Button
raised
colored
name="device_hub"
onClick={() => history.push(`/addons/create/${provider.name}`)}
title="Configure"
>
Configure
</Button>
) : (
''
)}
</ListItemAction>
</ListItem>
))}
</List>
</Card>
</div>
);
};
AddonListComponent.propTypes = {
addons: PropTypes.array.isRequired,
providers: PropTypes.array.isRequired,
fetchAddons: PropTypes.func.isRequired,
removeAddon: PropTypes.func.isRequired,
toggleAddon: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
hasPermission: PropTypes.func.isRequired,
};
export default AddonListComponent;

View File

@ -0,0 +1,32 @@
import { connect } from 'react-redux';
import AddonsListComponent from './list-component.jsx';
import { fetchAddons, removeAddon, updateAddon } from '../../store/addons/actions';
import { hasPermission } from '../../permissions';
const mapStateToProps = state => {
const list = state.addons.toJS();
return {
addons: list.addons,
providers: list.providers,
hasPermission: hasPermission.bind(null, state.user.get('profile')),
};
};
const mapDispatchToProps = dispatch => ({
removeAddon: addon => {
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to remove this addon?')) {
removeAddon(addon)(dispatch);
}
},
fetchAddons: () => fetchAddons()(dispatch),
toggleAddon: addon => {
const updatedAddon = { ...addon, enabled: !addon.enabled };
return updateAddon(updatedAddon)(dispatch);
},
});
const AddonsListContainer = connect(mapStateToProps, mapDispatchToProps)(AddonsListComponent);
export default AddonsListContainer;

View File

@ -98,4 +98,8 @@
top: 56px;
right: 24px;
z-index: 2;
}
.error {
color: #d50000;
}

View File

@ -11,8 +11,19 @@ function FeatureTagComponent({ tags, tagTypes, featureToggleName, untagFeature }
const tagIcon = typeName => {
let tagType = tagTypes.find(type => type.name === typeName);
const style = { width: '20px', height: '20px', margin: '0' };
if (tagType && tagType.icon) {
return <Icon name={tagType.icon} />;
switch (tagType.name) {
case 'slack':
return <img style={style} src="public/slack.svg" />;
case 'jira':
return <img style={style} src="public/jira.svg" />;
case 'webhook':
return <img style={style} src="public/webhooks.svg" />;
default:
return <Icon name={tagType.icon} />;
}
} else {
return <span>{typeName[0].toUpperCase()}</span>;
}
@ -21,12 +32,12 @@ function FeatureTagComponent({ tags, tagTypes, featureToggleName, untagFeature }
const renderTag = t => (
<Chip
onClose={() => onUntagFeature(t)}
title={t.value}
title={`Type: ${t.type} \nValue: ${t.value}`}
key={`${t.type}:${t.value}`}
style={{ marginRight: '3px', fontSize: '0.8em' }}
>
<ChipContact className="mdl-color--grey-500">{tagIcon(t.type)}</ChipContact>
{t.value}
<ChipContact>{tagIcon(t.type)}</ChipContact>
<span style={{ paddingRight: '3px' }}>{t.value}</span>
</Chip>
);

View File

@ -8,6 +8,19 @@ import { formatFullDateTimeWithLocale } from '../common/util';
import styles from './history.module.scss';
const getName = name => {
if (name) {
return (
<React.Fragment>
<dt>Name: </dt>
<dd>{name}</dd>
</React.Fragment>
);
} else {
return null;
}
};
const HistoryMeta = ({ entry, timeFormatted }) => (
<div>
<dl>
@ -17,8 +30,7 @@ const HistoryMeta = ({ entry, timeFormatted }) => (
<dd title={entry.createdBy}>{entry.createdBy}</dd>
<dt>Type: </dt>
<dd>{entry.type}</dd>
<dt>Name: </dt>
<dd>{entry.data.name}</dd>
{getName(entry.data.name)}
</dl>
<strong>Change</strong>
<HistoryItemDiff entry={entry} />

View File

@ -106,6 +106,19 @@ exports[`should render DrawerMenu 1`] = `
Tag types
</a>
<a
aria-current={null}
className="navigationLink mdl-color-text--grey-900"
href="/addons"
onClick={[Function]}
>
<react-mdl-Icon
className="navigationIcon"
name="device_hub"
/>
Addons
</a>
<a
aria-current={null}
className="navigationLink mdl-color-text--grey-900"
@ -234,6 +247,19 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
Tag types
</a>
<a
aria-current={null}
className="navigationLink mdl-color-text--grey-900"
href="/addons"
onClick={[Function]}
>
<react-mdl-Icon
className="navigationIcon"
name="device_hub"
/>
Addons
</a>
<a
aria-current={null}
className="navigationLink mdl-color-text--grey-900"

View File

@ -50,6 +50,13 @@ exports[`should render DrawerMenu 1`] = `
>
Tag types
</a>
<a
aria-current={null}
href="/addons"
onClick={[Function]}
>
Addons
</a>
<a
aria-current={null}
href="/logout"
@ -161,6 +168,13 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
>
Tag types
</a>
<a
aria-current={null}
href="/addons"
onClick={[Function]}
>
Addons
</a>
<a
aria-current={null}
href="/logout"

View File

@ -38,6 +38,13 @@ Array [
"path": "/tag-types",
"title": "Tag types",
},
Object {
"component": [Function],
"hidden": false,
"icon": "device_hub",
"path": "/addons",
"title": "Addons",
},
Object {
"component": [Function],
"icon": "exit_to_app",
@ -196,6 +203,25 @@ Array [
"path": "/tags",
"title": "Tags",
},
Object {
"component": [Function],
"parent": "/addons",
"path": "/addons/create/:provider",
"title": "Create",
},
Object {
"component": [Function],
"parent": "/addons",
"path": "/addons/edit/:id",
"title": "Edit",
},
Object {
"component": [Function],
"hidden": false,
"icon": "device_hub",
"path": "/addons",
"title": "Addons",
},
Object {
"component": [Function],
"icon": "exit_to_app",

View File

@ -1,12 +1,12 @@
import { routes, baseRoutes, getRoute } from '../routes';
test('returns all defined routes', () => {
expect(routes.length).toEqual(25);
expect(routes.length).toEqual(28);
expect(routes).toMatchSnapshot();
});
test('returns all baseRoutes', () => {
expect(baseRoutes.length).toEqual(7);
expect(baseRoutes.length).toEqual(8);
expect(baseRoutes).toMatchSnapshot();
});

View File

@ -23,7 +23,9 @@ import CreateTagType from '../../page/tag-types/create';
import EditTagType from '../../page/tag-types/edit';
import ListTags from '../../page/tags';
import CreateTag from '../../page/tags/create';
import Addons from '../../page/addons';
import AddonsCreate from '../../page/addons/create';
import AddonsEdit from '../../page/addons/edit';
export const routes = [
// Features
{ path: '/features/create', parent: '/features', title: 'Create', component: CreateFeatureToggle },
@ -70,6 +72,11 @@ export const routes = [
{ path: '/tags/create', parent: '/tags', title: 'Create', component: CreateTag },
{ path: '/tags', title: 'Tags', icon: 'label', component: ListTags, hidden: true },
// Addons
{ path: '/addons/create/:provider', parent: '/addons', title: 'Create', component: AddonsCreate },
{ path: '/addons/edit/:id', parent: '/addons', title: 'Edit', component: AddonsEdit },
{ path: '/addons', title: 'Addons', icon: 'device_hub', component: Addons, hidden: false },
{ path: '/logout', title: 'Sign out', icon: 'exit_to_app', component: LogoutFeatures },
];

View File

@ -0,0 +1,14 @@
import React from 'react';
import AddonForm from '../../component/addons/form-addon-container';
import PropTypes from 'prop-types';
const render = ({ match: { params }, history }) => (
<AddonForm provider={params.provider} title="Configure addon" history={history} />
);
render.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -0,0 +1,14 @@
import React from 'react';
import AddonForm from '../../component/addons/form-addon-container';
import PropTypes from 'prop-types';
const render = ({ match: { params }, history }) => (
<AddonForm addonId={params.id} title="Edit addon" history={history} />
);
render.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -0,0 +1,11 @@
import React from 'react';
import Addons from '../../component/addons/list-container';
import PropTypes from 'prop-types';
const render = ({ history }) => <Addons history={history} />;
render.propTypes = {
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -17,6 +17,9 @@ export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE';
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
export const CREATE_TAG = 'CREATE_TAG';
export const DELETE_TAG = 'DELETE_TAG';
export const CREATE_ADDON = 'CREATE_ADDON';
export const UPDATE_ADDON = 'UPDATE_ADDON';
export const DELETE_ADDON = 'DELETE_ADDON';
export function hasPermission(user, permission) {
return (

View File

@ -0,0 +1,189 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should add addon-config 1`] = `
Object {
"addons": Array [
Object {
"description": null,
"enabled": true,
"events": Array [
"feature-updated",
"feature-created",
],
"id": 1,
"parameters": Object {
"bodyTemplate": "{'name': '{{event.data.name}}' }",
"url": "http://localhost:4242/webhook",
},
"provider": "webhook",
},
],
"providers": Array [
Object {
"displayName": "Webhook",
"events": Array [
"feature-created",
"feature-updated",
"feature-archived",
"feature-revived",
],
"name": "webhook",
"parameters": Array [
Object {
"displayName": "Webhook URL",
"name": "url",
"type": "string",
},
Object {
"displayName": "Unleash Admin UI url",
"name": "unleashUrl",
"type": "text",
},
Object {
"description": "You may format the body using a mustache template.",
"displayName": "Body template",
"name": "bodyTemplate",
"type": "text",
},
],
},
],
}
`;
exports[`should be default state 1`] = `
Object {
"addons": Array [],
"providers": Array [],
}
`;
exports[`should be merged state all 1`] = `
Object {
"addons": Array [],
"providers": Array [
Object {
"displayName": "Webhook",
"events": Array [
"feature-created",
"feature-updated",
"feature-archived",
"feature-revived",
],
"name": "webhook",
"parameters": Array [
Object {
"displayName": "Webhook URL",
"name": "url",
"type": "string",
},
Object {
"displayName": "Unleash Admin UI url",
"name": "unleashUrl",
"type": "text",
},
Object {
"description": "You may format the body using a mustache template.",
"displayName": "Body template",
"name": "bodyTemplate",
"type": "text",
},
],
},
],
}
`;
exports[`should clear addon-config on logout 1`] = `
Object {
"addons": Array [],
"providers": Array [],
}
`;
exports[`should remove addon-config 1`] = `
Object {
"addons": Array [],
"providers": Array [
Object {
"displayName": "Webhook",
"events": Array [
"feature-created",
"feature-updated",
"feature-archived",
"feature-revived",
],
"name": "webhook",
"parameters": Array [
Object {
"displayName": "Webhook URL",
"name": "url",
"type": "string",
},
Object {
"displayName": "Unleash Admin UI url",
"name": "unleashUrl",
"type": "text",
},
Object {
"description": "You may format the body using a mustache template.",
"displayName": "Body template",
"name": "bodyTemplate",
"type": "text",
},
],
},
],
}
`;
exports[`should update addon-config 1`] = `
Object {
"addons": Array [
Object {
"description": "new desc",
"enabled": false,
"events": Array [
"feature-updated",
"feature-created",
],
"id": 1,
"parameters": Object {
"bodyTemplate": "{'name': '{{event.data.name}}' }",
"url": "http://localhost:4242/webhook",
},
"provider": "webhook",
},
],
"providers": Array [
Object {
"displayName": "Webhook",
"events": Array [
"feature-created",
"feature-updated",
"feature-archived",
"feature-revived",
],
"name": "webhook",
"parameters": Array [
Object {
"displayName": "Webhook URL",
"name": "url",
"type": "string",
},
Object {
"displayName": "Unleash Admin UI url",
"name": "unleashUrl",
"type": "text",
},
Object {
"description": "You may format the body using a mustache template.",
"displayName": "Body template",
"name": "bodyTemplate",
"type": "text",
},
],
},
],
}
`;

View File

@ -0,0 +1,113 @@
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import fetchMock from 'fetch-mock';
import {
RECEIVE_ADDON_CONFIG,
ERRPR_RECEIVE_ADDON_CONFIG,
REMOVE_ADDON_CONFIG,
UPDATE_ADDON_CONFIG,
ADD_ADDON_CONFIG,
fetchAddons,
removeAddon,
updateAddon,
createAddon,
} from '../actions';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
afterEach(() => {
fetchMock.restore();
});
test('creates RECEIVE_ADDON_CONFIG when fetching addons has been done', () => {
fetchMock.getOnce('api/admin/addons', {
body: { addons: { providers: [{ name: 'webhook' }] } },
headers: { 'content-type': 'application/json' },
});
const expectedActions = [{ type: RECEIVE_ADDON_CONFIG, value: { addons: { providers: [{ name: 'webhook' }] } } }];
const store = mockStore({ addons: [] });
return store.dispatch(fetchAddons()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
test('creates RECEIVE_ADDON_CONFIG_ when fetching addons has been done', () => {
fetchMock.getOnce('api/admin/addons', {
body: { message: 'Server error' },
headers: { 'content-type': 'application/json' },
status: 500,
});
const store = mockStore({ addons: [] });
return store.dispatch(fetchAddons()).catch(e => {
// return of async actions
expect(store.getActions()[0].error.type).toEqual(ERRPR_RECEIVE_ADDON_CONFIG);
expect(e.message).toEqual('Unexpected exception when talking to unleash-api');
});
});
test('creates REMOVE_ADDON_CONFIG when delete addon has been done', () => {
const addon = {
id: 1,
provider: 'webhook',
};
fetchMock.deleteOnce('api/admin/addons/1', {
status: 200,
});
const expectedActions = [{ type: REMOVE_ADDON_CONFIG, value: addon }];
const store = mockStore({ addons: [] });
return store.dispatch(removeAddon(addon)).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
test('creates UPDATE_ADDON_CONFIG when delete addon has been done', () => {
const addon = {
id: 1,
provider: 'webhook',
};
fetchMock.putOnce('api/admin/addons/1', {
headers: { 'content-type': 'application/json' },
status: 200,
body: addon,
});
const expectedActions = [{ type: UPDATE_ADDON_CONFIG, value: addon }];
const store = mockStore({ addons: [] });
return store.dispatch(updateAddon(addon)).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
test('creates ADD_ADDON_CONFIG when delete addon has been done', () => {
const addon = {
provider: 'webhook',
};
fetchMock.postOnce('api/admin/addons', {
headers: { 'content-type': 'application/json' },
status: 200,
body: addon,
});
const expectedActions = [{ type: ADD_ADDON_CONFIG, value: addon }];
const store = mockStore({ addons: [] });
return store.dispatch(createAddon(addon)).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});

View File

@ -0,0 +1,54 @@
import reducer from '../index';
import { RECEIVE_ADDON_CONFIG, ADD_ADDON_CONFIG, REMOVE_ADDON_CONFIG, UPDATE_ADDON_CONFIG } from '../actions';
import { addonSimple, addonsWithConfig, addonConfig } from './data';
import { USER_LOGOUT } from '../../user/actions';
test('should be default state', () => {
const state = reducer(undefined, {});
expect(state.toJS()).toMatchSnapshot();
});
test('should be merged state all', () => {
const state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonSimple });
expect(state.toJS()).toMatchSnapshot();
});
test('should add addon-config', () => {
let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonSimple });
state = reducer(state, { type: ADD_ADDON_CONFIG, value: addonConfig });
const data = state.toJS();
expect(data).toMatchSnapshot();
expect(data.addons.length).toBe(1);
});
test('should remove addon-config', () => {
let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonsWithConfig });
state = reducer(state, { type: REMOVE_ADDON_CONFIG, value: addonConfig });
const data = state.toJS();
expect(data).toMatchSnapshot();
expect(data.addons.length).toBe(0);
});
test('should update addon-config', () => {
const updateAdddonConfig = { ...addonConfig, description: 'new desc', enabled: false };
let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonsWithConfig });
state = reducer(state, { type: UPDATE_ADDON_CONFIG, value: updateAdddonConfig });
const data = state.toJS();
expect(data).toMatchSnapshot();
expect(data.addons.length).toBe(1);
expect(data.addons[0].description).toBe('new desc');
});
test('should clear addon-config on logout', () => {
let state = reducer(undefined, { type: RECEIVE_ADDON_CONFIG, value: addonsWithConfig });
state = reducer(state, { type: USER_LOGOUT });
const data = state.toJS();
expect(data).toMatchSnapshot();
expect(data.addons.length).toBe(0);
expect(data.providers.length).toBe(0);
});

View File

@ -0,0 +1,69 @@
export const addonSimple = {
addons: [],
providers: [
{
name: 'webhook',
displayName: 'Webhook',
parameters: [
{
name: 'url',
displayName: 'Webhook URL',
type: 'string',
},
{
name: 'unleashUrl',
displayName: 'Unleash Admin UI url',
type: 'text',
},
{
name: 'bodyTemplate',
displayName: 'Body template',
description: 'You may format the body using a mustache template.',
type: 'text',
},
],
events: ['feature-created', 'feature-updated', 'feature-archived', 'feature-revived'],
},
],
};
export const addonConfig = {
id: 1,
provider: 'webhook',
enabled: true,
description: null,
parameters: {
url: 'http://localhost:4242/webhook',
bodyTemplate: "{'name': '{{event.data.name}}' }",
},
events: ['feature-updated', 'feature-created'],
};
export const addonsWithConfig = {
addons: [addonConfig],
providers: [
{
name: 'webhook',
displayName: 'Webhook',
parameters: [
{
name: 'url',
displayName: 'Webhook URL',
type: 'string',
},
{
name: 'unleashUrl',
displayName: 'Unleash Admin UI url',
type: 'text',
},
{
name: 'bodyTemplate',
displayName: 'Body template',
description: 'You may format the body using a mustache template.',
type: 'text',
},
],
events: ['feature-created', 'feature-updated', 'feature-archived', 'feature-revived'],
},
],
};

View File

@ -0,0 +1,51 @@
import api from './api';
import { dispatchAndThrow } from '../util';
export const RECEIVE_ADDON_CONFIG = 'RECEIVE_ADDON_CONFIG';
export const ERROR_RECEIVE_ADDON_CONFIG = 'ERROR_RECEIVE_ADDON_CONFIG';
export const REMOVE_ADDON_CONFIG = 'REMOVE_ADDON_CONFIG';
export const ERROR_REMOVING_ADDON_CONFIG = 'ERROR_REMOVING_ADDON_CONFIG';
export const ADD_ADDON_CONFIG = 'ADD_ADDON_CONFIG';
export const ERROR_ADD_ADDON_CONFIG = 'ERROR_ADD_ADDON_CONFIG';
export const UPDATE_ADDON_CONFIG = 'UPDATE_ADDON_CONFIG';
export const ERROR_UPDATE_ADDON_CONFIG = 'ERROR_UPDATE_ADDON_CONFIG';
// const receiveAddonConfig = value => ({ type: RECEIVE_ADDON_CONFIG, value });
const addAddonConfig = value => ({ type: ADD_ADDON_CONFIG, value });
const updateAdddonConfig = value => ({ type: UPDATE_ADDON_CONFIG, value });
const removeAddonconfig = value => ({ type: REMOVE_ADDON_CONFIG, value });
const success = (dispatch, type) => value => dispatch({ type, value });
export function fetchAddons() {
return dispatch =>
api
.fetchAll()
.then(success(dispatch, RECEIVE_ADDON_CONFIG))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ADDON_CONFIG));
}
export function removeAddon(addon) {
return dispatch =>
api
.remove(addon)
.then(() => dispatch(removeAddonconfig(addon)))
.catch(dispatchAndThrow(dispatch, ERROR_REMOVING_ADDON_CONFIG));
}
export function createAddon(addon) {
return dispatch =>
api
.create(addon)
.then(res => res.json())
.then(value => dispatch(addAddonConfig(value)))
.catch(dispatchAndThrow(dispatch, ERROR_ADD_ADDON_CONFIG));
}
export function updateAddon(addon) {
return dispatch =>
api
.update(addon)
.then(() => dispatch(updateAdddonConfig(addon)))
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_ADDON_CONFIG));
}

View File

@ -0,0 +1,42 @@
import { throwIfNotSuccess, headers } from '../api-helper';
const URI = 'api/admin/addons';
function fetchAll() {
return fetch(URI, { credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
function create(addonConfig) {
return fetch(URI, {
method: 'POST',
headers,
body: JSON.stringify(addonConfig),
credentials: 'include',
}).then(throwIfNotSuccess);
}
function update(addonConfig) {
return fetch(`${URI}/${addonConfig.id}`, {
method: 'PUT',
headers,
body: JSON.stringify(addonConfig),
credentials: 'include',
}).then(throwIfNotSuccess);
}
function remove(addonConfig) {
return fetch(`${URI}/${addonConfig.id}`, {
method: 'DELETE',
headers,
credentials: 'include',
}).then(throwIfNotSuccess);
}
export default {
fetchAll,
create,
update,
remove,
};

View File

@ -0,0 +1,33 @@
import { Map as $Map, List, fromJS } from 'immutable';
import { RECEIVE_ADDON_CONFIG, ADD_ADDON_CONFIG, REMOVE_ADDON_CONFIG, UPDATE_ADDON_CONFIG } from './actions';
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
function getInitState() {
return new $Map({
providers: new List(),
addons: new List(),
});
}
const strategies = (state = getInitState(), action) => {
switch (action.type) {
case RECEIVE_ADDON_CONFIG:
return fromJS(action.value);
case ADD_ADDON_CONFIG: {
return state.update('addons', arr => arr.push(fromJS(action.value)));
}
case REMOVE_ADDON_CONFIG:
return state.update('addons', arr => arr.filter(a => a.get('id') !== action.value.id));
case UPDATE_ADDON_CONFIG: {
const index = state.get('addons').findIndex(item => item.get('id') === action.value.id);
return state.setIn(['addons', index], fromJS(action.value));
}
case USER_LOGOUT:
case USER_LOGIN:
return getInitState();
default:
return state;
}
};
export default strategies;

View File

@ -15,6 +15,7 @@ import applications from './application';
import uiConfig from './ui-config';
import context from './context';
import projects from './project';
import addons from './addons';
const unleashStore = combineReducers({
features,
@ -33,6 +34,7 @@ const unleashStore = combineReducers({
uiConfig,
context,
projects,
addons,
});
export default unleashStore;

View File

@ -17,3 +17,5 @@ export function dispatchAndThrow(dispatch, type) {
throw error;
};
}
export const success = (dispatch, type, val) => value => dispatch({ type, value: val ? val : value });

View File

@ -9,6 +9,13 @@
dependencies:
"@babel/highlight" "^7.8.3"
"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
dependencies:
"@babel/highlight" "^7.10.4"
"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0":
version "7.9.0"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c"
@ -18,6 +25,27 @@
invariant "^2.2.4"
semver "^5.5.0"
"@babel/core@^7.0.0":
version "7.12.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd"
integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/generator" "^7.12.10"
"@babel/helper-module-transforms" "^7.12.1"
"@babel/helpers" "^7.12.5"
"@babel/parser" "^7.12.10"
"@babel/template" "^7.12.7"
"@babel/traverse" "^7.12.10"
"@babel/types" "^7.12.10"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.1"
json5 "^2.1.2"
lodash "^4.17.19"
semver "^5.4.1"
source-map "^0.5.0"
"@babel/core@^7.1.0", "@babel/core@^7.7.5", "@babel/core@^7.9.0":
version "7.9.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
@ -40,6 +68,15 @@
semver "^5.4.1"
source-map "^0.5.0"
"@babel/generator@^7.12.10", "@babel/generator@^7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af"
integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==
dependencies:
"@babel/types" "^7.12.11"
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/generator@^7.4.0", "@babel/generator@^7.9.0", "@babel/generator@^7.9.5":
version "7.9.5"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9"
@ -131,6 +168,15 @@
"@babel/traverse" "^7.8.3"
"@babel/types" "^7.8.3"
"@babel/helper-function-name@^7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42"
integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==
dependencies:
"@babel/helper-get-function-arity" "^7.12.10"
"@babel/template" "^7.12.7"
"@babel/types" "^7.12.11"
"@babel/helper-function-name@^7.8.3", "@babel/helper-function-name@^7.9.5":
version "7.9.5"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c"
@ -140,6 +186,13 @@
"@babel/template" "^7.8.3"
"@babel/types" "^7.9.5"
"@babel/helper-get-function-arity@^7.12.10":
version "7.12.10"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf"
integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==
dependencies:
"@babel/types" "^7.12.10"
"@babel/helper-get-function-arity@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5"
@ -154,6 +207,13 @@
dependencies:
"@babel/types" "^7.8.3"
"@babel/helper-member-expression-to-functions@^7.12.7":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855"
integrity sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==
dependencies:
"@babel/types" "^7.12.7"
"@babel/helper-member-expression-to-functions@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c"
@ -168,6 +228,28 @@
dependencies:
"@babel/types" "^7.8.3"
"@babel/helper-module-imports@^7.12.1":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb"
integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==
dependencies:
"@babel/types" "^7.12.5"
"@babel/helper-module-transforms@^7.12.1":
version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c"
integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==
dependencies:
"@babel/helper-module-imports" "^7.12.1"
"@babel/helper-replace-supers" "^7.12.1"
"@babel/helper-simple-access" "^7.12.1"
"@babel/helper-split-export-declaration" "^7.11.0"
"@babel/helper-validator-identifier" "^7.10.4"
"@babel/template" "^7.10.4"
"@babel/traverse" "^7.12.1"
"@babel/types" "^7.12.1"
lodash "^4.17.19"
"@babel/helper-module-transforms@^7.9.0":
version "7.9.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5"
@ -181,6 +263,13 @@
"@babel/types" "^7.9.0"
lodash "^4.17.13"
"@babel/helper-optimise-call-expression@^7.12.10":
version "7.12.10"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz#94ca4e306ee11a7dd6e9f42823e2ac6b49881e2d"
integrity sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==
dependencies:
"@babel/types" "^7.12.10"
"@babel/helper-optimise-call-expression@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9"
@ -211,6 +300,16 @@
"@babel/traverse" "^7.8.3"
"@babel/types" "^7.8.3"
"@babel/helper-replace-supers@^7.12.1":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz#ea511658fc66c7908f923106dd88e08d1997d60d"
integrity sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==
dependencies:
"@babel/helper-member-expression-to-functions" "^7.12.7"
"@babel/helper-optimise-call-expression" "^7.12.10"
"@babel/traverse" "^7.12.10"
"@babel/types" "^7.12.11"
"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6":
version "7.8.6"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8"
@ -221,6 +320,13 @@
"@babel/traverse" "^7.8.6"
"@babel/types" "^7.8.6"
"@babel/helper-simple-access@^7.12.1":
version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136"
integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==
dependencies:
"@babel/types" "^7.12.1"
"@babel/helper-simple-access@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae"
@ -229,6 +335,13 @@
"@babel/template" "^7.8.3"
"@babel/types" "^7.8.3"
"@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a"
integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==
dependencies:
"@babel/types" "^7.12.11"
"@babel/helper-split-export-declaration@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9"
@ -236,6 +349,11 @@
dependencies:
"@babel/types" "^7.8.3"
"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
"@babel/helper-validator-identifier@^7.9.0", "@babel/helper-validator-identifier@^7.9.5":
version "7.9.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80"
@ -251,6 +369,15 @@
"@babel/traverse" "^7.8.3"
"@babel/types" "^7.8.3"
"@babel/helpers@^7.12.5":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e"
integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==
dependencies:
"@babel/template" "^7.10.4"
"@babel/traverse" "^7.12.5"
"@babel/types" "^7.12.5"
"@babel/helpers@^7.9.0":
version "7.9.2"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f"
@ -260,6 +387,15 @@
"@babel/traverse" "^7.9.0"
"@babel/types" "^7.9.0"
"@babel/highlight@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143"
integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==
dependencies:
"@babel/helper-validator-identifier" "^7.10.4"
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/highlight@^7.8.3":
version "7.9.0"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079"
@ -274,6 +410,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==
"@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79"
integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==
"@babel/plugin-proposal-async-generator-functions@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f"
@ -856,6 +997,13 @@
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4":
version "7.9.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
@ -870,6 +1018,15 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.10.4", "@babel/template@^7.12.7":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/parser" "^7.12.7"
"@babel/types" "^7.12.7"
"@babel/template@^7.4.0", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
version "7.8.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
@ -894,6 +1051,21 @@
globals "^11.1.0"
lodash "^4.17.13"
"@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.5":
version "7.12.12"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376"
integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==
dependencies:
"@babel/code-frame" "^7.12.11"
"@babel/generator" "^7.12.11"
"@babel/helper-function-name" "^7.12.11"
"@babel/helper-split-export-declaration" "^7.12.11"
"@babel/parser" "^7.12.11"
"@babel/types" "^7.12.12"
debug "^4.1.0"
globals "^11.1.0"
lodash "^4.17.19"
"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5":
version "7.9.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444"
@ -903,6 +1075,15 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7":
version "7.12.12"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299"
integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==
dependencies:
"@babel/helper-validator-identifier" "^7.12.11"
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@cnakazawa/watch@^1.0.3":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
@ -2050,14 +2231,6 @@ babel-preset-jest@^25.3.0:
babel-plugin-jest-hoist "^25.2.6"
babel-preset-current-node-syntax "^0.1.2"
babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -2816,11 +2989,6 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
core-js@^2.4.0:
version "2.6.11"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
core-js@^3.0.0:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
@ -4042,12 +4210,13 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fetch-mock@^9.4.0:
version "9.4.0"
resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-9.4.0.tgz#9be073577bcfa57af714ca91f7536aff8450ec88"
integrity sha512-tqnFmcjYheW5Z9zOPRVY+ZXjB/QWCYtPiOrYGEsPgKfpGHco97eaaj7Rv9MjK7PVWG4rWfv6t2IgQAzDQizBZA==
fetch-mock@^9.11.0:
version "9.11.0"
resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-9.11.0.tgz#371c6fb7d45584d2ae4a18ee6824e7ad4b637a3f"
integrity sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==
dependencies:
babel-runtime "^6.26.0"
"@babel/core" "^7.0.0"
"@babel/runtime" "^7.0.0"
core-js "^3.0.0"
debug "^4.1.1"
glob-to-regexp "^0.4.0"
@ -8133,11 +8302,6 @@ regenerate@^1.4.0:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.4:
version "0.13.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"