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:
parent
f7f58fa0a7
commit
3a6fa577bc
@ -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
1
frontend/public/jira.svg
Normal 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 |
1
frontend/public/slack.svg
Normal file
1
frontend/public/slack.svg
Normal 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 |
1
frontend/public/webhooks.svg
Normal file
1
frontend/public/webhooks.svg
Normal 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 |
177
frontend/src/component/addons/form-addon-component.jsx
Normal file
177
frontend/src/component/addons/form-addon-component.jsx
Normal 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}
|
||||
<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;
|
55
frontend/src/component/addons/form-addon-container.js
Normal file
55
frontend/src/component/addons/form-addon-container.js
Normal 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;
|
32
frontend/src/component/addons/form-addon-events.jsx
Normal file
32
frontend/src/component/addons/form-addon-events.jsx
Normal 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;
|
82
frontend/src/component/addons/form-addon-parameters.jsx
Normal file
82
frontend/src/component/addons/form-addon-parameters.jsx
Normal 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;
|
117
frontend/src/component/addons/list-component.jsx
Normal file
117
frontend/src/component/addons/list-component.jsx
Normal 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>
|
||||
</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;
|
32
frontend/src/component/addons/list-container.jsx
Normal file
32
frontend/src/component/addons/list-container.jsx
Normal 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;
|
@ -98,4 +98,8 @@
|
||||
top: 56px;
|
||||
right: 24px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #d50000;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
||||
|
@ -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} />
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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 },
|
||||
];
|
||||
|
||||
|
14
frontend/src/page/addons/create.js
Normal file
14
frontend/src/page/addons/create.js
Normal 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;
|
14
frontend/src/page/addons/edit.js
Normal file
14
frontend/src/page/addons/edit.js
Normal 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;
|
11
frontend/src/page/addons/index.js
Normal file
11
frontend/src/page/addons/index.js
Normal 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;
|
@ -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 (
|
||||
|
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
113
frontend/src/store/addons/__tests__/addons-actions.test.js
Normal file
113
frontend/src/store/addons/__tests__/addons-actions.test.js
Normal 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);
|
||||
});
|
||||
});
|
54
frontend/src/store/addons/__tests__/addons-store.test.js
Normal file
54
frontend/src/store/addons/__tests__/addons-store.test.js
Normal 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);
|
||||
});
|
69
frontend/src/store/addons/__tests__/data.js
Normal file
69
frontend/src/store/addons/__tests__/data.js
Normal 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'],
|
||||
},
|
||||
],
|
||||
};
|
51
frontend/src/store/addons/actions.js
Normal file
51
frontend/src/store/addons/actions.js
Normal 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));
|
||||
}
|
42
frontend/src/store/addons/api.js
Normal file
42
frontend/src/store/addons/api.js
Normal 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,
|
||||
};
|
33
frontend/src/store/addons/index.js
Normal file
33
frontend/src/store/addons/index.js
Normal 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;
|
@ -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;
|
||||
|
@ -17,3 +17,5 @@ export function dispatchAndThrow(dispatch, type) {
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
export const success = (dispatch, type, val) => value => dispatch({ type, value: val ? val : value });
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user