mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
fix: one and only one front (#244)
Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai> Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
8a72a7ef5d
commit
5342c86b60
@ -112,8 +112,8 @@ The latest version of this document is always available in
|
|||||||
- feat: added time-ago to toggle-list
|
- feat: added time-ago to toggle-list
|
||||||
- feat: Add stale marking of feature toggles
|
- feat: Add stale marking of feature toggles
|
||||||
- feat: add support for toggle type (#220)
|
- feat: add support for toggle type (#220)
|
||||||
- feat: stort by stale
|
- feat: sort by stale
|
||||||
- fix: imporve type-chip color
|
- fix: improve type-chip color
|
||||||
- fix: some ux cleanup for toggle types
|
- fix: some ux cleanup for toggle types
|
||||||
|
|
||||||
# [3.4.0]
|
# [3.4.0]
|
||||||
@ -124,7 +124,7 @@ The latest version of this document is always available in
|
|||||||
- fix: upgrade react-dnd to version 11.1.3
|
- fix: upgrade react-dnd to version 11.1.3
|
||||||
- fix: Update react-dnd to the latest version 🚀 (#213)
|
- fix: Update react-dnd to the latest version 🚀 (#213)
|
||||||
- fix: read unleash version from ui-config (#219)
|
- fix: read unleash version from ui-config (#219)
|
||||||
- fix: flag inital context fields
|
- fix: flag initial context fields
|
||||||
|
|
||||||
|
|
||||||
# [3.3.5]
|
# [3.3.5]
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 41 KiB |
BIN
frontend/public/favicon_old.ico
Normal file
BIN
frontend/public/favicon_old.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
frontend/public/logo_old.png
Normal file
BIN
frontend/public/logo_old.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
@ -3,7 +3,7 @@ import FormComponent from './form-addon-component';
|
|||||||
import { updateAddon, createAddon, fetchAddons } from '../../store/addons/actions';
|
import { updateAddon, createAddon, fetchAddons } from '../../store/addons/actions';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
// Required for to fill the inital form.
|
// Required for to fill the initial form.
|
||||||
const DEFAULT_DATA = {
|
const DEFAULT_DATA = {
|
||||||
provider: '',
|
provider: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -13,8 +13,8 @@ const Empty = () => (
|
|||||||
you will require a Client SDK.
|
you will require a Client SDK.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
You can read more about the available Client SDKs in the{' '}
|
You can read more about how to use Unleash in your application in the{' '}
|
||||||
<a href="https://unleash.github.io/docs/client_sdk">documentation.</a>
|
<a href="https://www.unleash-hosted.com/docs/use-feature-toggle">documentation.</a>
|
||||||
</CardText>
|
</CardText>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
2
frontend/src/component/common/flags.js
Normal file
2
frontend/src/component/common/flags.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const P = 'P';
|
||||||
|
export const C = 'C';
|
@ -140,14 +140,15 @@ IconLink.propTypes = {
|
|||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DropdownButton = ({ label, id, className = styles.dropdownButton, title }) => (
|
export const DropdownButton = ({ label, id, className = styles.dropdownButton, title, style }) => (
|
||||||
<Button id={id} className={className} title={title}>
|
<Button id={id} className={className} title={title} style={style}>
|
||||||
{label}
|
{label}
|
||||||
<Icon name="arrow_drop_down" className="mdl-color-text--grey-600" />
|
<Icon name="arrow_drop_down" className="mdl-color-text--grey-600" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
DropdownButton.propTypes = {
|
DropdownButton.propTypes = {
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
};
|
};
|
||||||
@ -185,3 +186,15 @@ export function calc(value, total, decimal) {
|
|||||||
export function getDisplayName(WrappedComponent) {
|
export function getDisplayName(WrappedComponent) {
|
||||||
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const selectStyles = {
|
||||||
|
control: provided => ({
|
||||||
|
...provided,
|
||||||
|
border: '1px solid #607d8b',
|
||||||
|
boxShadow: '0',
|
||||||
|
':hover': {
|
||||||
|
borderColor: '#607d8b',
|
||||||
|
boxShadow: '0 0 0 1px #607d8b',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
@ -6,6 +6,9 @@ const mapStateToProps = (state, props) => {
|
|||||||
const contextFieldBase = { name: '', description: '', legalValues: [] };
|
const contextFieldBase = { name: '', description: '', legalValues: [] };
|
||||||
const field = state.context.toJS().find(n => n.name === props.contextFieldName);
|
const field = state.context.toJS().find(n => n.name === props.contextFieldName);
|
||||||
const contextField = Object.assign(contextFieldBase, field);
|
const contextField = Object.assign(contextFieldBase, field);
|
||||||
|
if (!field) {
|
||||||
|
contextField.initial = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contextField,
|
contextField,
|
||||||
|
@ -5,6 +5,14 @@ import { Button, Chip, Textfield, Card, CardTitle, CardText, CardActions, Checkb
|
|||||||
import { FormButtons, styles as commonStyles } from '../common';
|
import { FormButtons, styles as commonStyles } from '../common';
|
||||||
import { trim } from '../common/util';
|
import { trim } from '../common/util';
|
||||||
|
|
||||||
|
const sortIgnoreCase = (a, b) => {
|
||||||
|
a = a.toLowerCase();
|
||||||
|
b = b.toLowerCase();
|
||||||
|
if (a === b) return 0;
|
||||||
|
if (a > b) return 1;
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
class AddContextComponent extends Component {
|
class AddContextComponent extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -17,7 +25,7 @@ class AddContextComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
static getDerivedStateFromProps(props, state) {
|
||||||
if (!state.contextField.name && props.contextField.name) {
|
if (state.contextField.initial && !props.contextField.initial) {
|
||||||
return { contextField: props.contextField };
|
return { contextField: props.contextField };
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@ -62,6 +70,10 @@ class AddContextComponent extends Component {
|
|||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { contextField, currentLegalValue, errors } = this.state;
|
const { contextField, currentLegalValue, errors } = this.state;
|
||||||
|
|
||||||
|
if (!currentLegalValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (contextField.legalValues.indexOf(currentLegalValue) !== -1) {
|
if (contextField.legalValues.indexOf(currentLegalValue) !== -1) {
|
||||||
errors.currentLegalValue = 'Duplicate legal value';
|
errors.currentLegalValue = 'Duplicate legal value';
|
||||||
this.setState({ errors });
|
this.setState({ errors });
|
||||||
@ -69,7 +81,7 @@ class AddContextComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const legalValues = contextField.legalValues.concat(trim(currentLegalValue));
|
const legalValues = contextField.legalValues.concat(trim(currentLegalValue));
|
||||||
contextField.legalValues = legalValues;
|
contextField.legalValues = legalValues.sort(sortIgnoreCase);
|
||||||
this.setState({
|
this.setState({
|
||||||
contextField,
|
contextField,
|
||||||
currentLegalValue: '',
|
currentLegalValue: '',
|
||||||
@ -148,7 +160,9 @@ class AddContextComponent extends Component {
|
|||||||
error={errors.currentLegalValue}
|
error={errors.currentLegalValue}
|
||||||
onChange={this.updateCurrentLegalValue}
|
onChange={this.updateCurrentLegalValue}
|
||||||
/>
|
/>
|
||||||
<Button onClick={this.addLegalValue}>Add</Button>
|
<Button onClick={this.addLegalValue} colored accent raised>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
<div>{contextField.legalValues.map(this.renderLegalValue)}</div>
|
<div>{contextField.legalValues.map(this.renderLegalValue)}</div>
|
||||||
</section>
|
</section>
|
||||||
<br />
|
<br />
|
||||||
|
@ -5,7 +5,7 @@ import MySelect from '../common/select';
|
|||||||
class FeatureTypeSelectComponent extends Component {
|
class FeatureTypeSelectComponent extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { fetchFeatureTypes, types } = this.props;
|
const { fetchFeatureTypes, types } = this.props;
|
||||||
if (types[0].inital && fetchFeatureTypes) {
|
if (types[0].initial && fetchFeatureTypes) {
|
||||||
this.props.fetchFeatureTypes();
|
this.props.fetchFeatureTypes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,13 @@ import { updateSettingForGroup } from '../../../store/settings/actions';
|
|||||||
import FeatureListComponent from './list-component';
|
import FeatureListComponent from './list-component';
|
||||||
import { hasPermission } from '../../../permissions';
|
import { hasPermission } from '../../../permissions';
|
||||||
|
|
||||||
|
function checkConstraints(strategy, regex) {
|
||||||
|
if (!strategy.constraints) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return strategy.constraints.some(c => c.values.some(v => regex.test(v)));
|
||||||
|
}
|
||||||
|
|
||||||
export const mapStateToPropsConfigurable = isFeature => state => {
|
export const mapStateToPropsConfigurable = isFeature => state => {
|
||||||
const featureMetrics = state.featureMetrics.toJS();
|
const featureMetrics = state.featureMetrics.toJS();
|
||||||
const settings = state.settings.toJS().feature || {};
|
const settings = state.settings.toJS().feature || {};
|
||||||
@ -19,6 +26,7 @@ export const mapStateToPropsConfigurable = isFeature => state => {
|
|||||||
const regex = new RegExp(settings.filter, 'i');
|
const regex = new RegExp(settings.filter, 'i');
|
||||||
features = features.filter(
|
features = features.filter(
|
||||||
feature =>
|
feature =>
|
||||||
|
feature.strategies.some(s => checkConstraints(s, regex)) ||
|
||||||
regex.test(feature.name) ||
|
regex.test(feature.name) ||
|
||||||
regex.test(feature.description) ||
|
regex.test(feature.description) ||
|
||||||
feature.strategies.some(s => s && s.name && regex.test(s.name)) ||
|
feature.strategies.some(s => s && s.name && regex.test(s.name)) ||
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { Menu, MenuItem } from 'react-mdl';
|
import { Menu, MenuItem } from 'react-mdl';
|
||||||
import { DropdownButton } from '../../common';
|
import { DropdownButton } from '../../common';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -13,13 +13,7 @@ function projectItem(selectedId, item) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectComponent({ fetchProjects, projects, currentProjectId, updateCurrentProject }) {
|
function ProjectComponent({ projects, currentProjectId, updateCurrentProject }) {
|
||||||
useEffect(() => {
|
|
||||||
if (projects[0].inital) {
|
|
||||||
fetchProjects();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function setProject(v) {
|
function setProject(v) {
|
||||||
const id = typeof v === 'string' ? v.trim() : '';
|
const id = typeof v === 'string' ? v.trim() : '';
|
||||||
updateCurrentProject(id);
|
updateCurrentProject(id);
|
||||||
@ -38,6 +32,7 @@ function ProjectComponent({ fetchProjects, projects, currentProjectId, updateCur
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
className="mdl-color--amber-50"
|
className="mdl-color--amber-50"
|
||||||
|
style={{ textTransform: 'none', fontWeight: 'normal' }}
|
||||||
id="project"
|
id="project"
|
||||||
label={`${curentProject.name}`}
|
label={`${curentProject.name}`}
|
||||||
title="Select project"
|
title="Select project"
|
||||||
|
@ -4,16 +4,16 @@ import MySelect from '../common/select';
|
|||||||
|
|
||||||
class ProjectSelectComponent extends Component {
|
class ProjectSelectComponent extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { fetchProjects, projects } = this.props;
|
const { fetchProjects, projects, enabled } = this.props;
|
||||||
if (projects[0].inital && fetchProjects) {
|
if (projects[0].initial && enabled) {
|
||||||
this.props.fetchProjects();
|
fetchProjects();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { value, projects, onChange, filled } = this.props;
|
const { value, projects, onChange, filled, enabled } = this.props;
|
||||||
|
|
||||||
if (!projects || projects.length === 1) {
|
if (!enabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ class ProjectSelectComponent extends Component {
|
|||||||
ProjectSelectComponent.propTypes = {
|
ProjectSelectComponent.propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
filled: PropTypes.bool,
|
filled: PropTypes.bool,
|
||||||
|
enabled: PropTypes.bool,
|
||||||
projects: PropTypes.array.isRequired,
|
projects: PropTypes.array.isRequired,
|
||||||
fetchProjects: PropTypes.func,
|
fetchProjects: PropTypes.func,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ProjectSelectComponent from './project-select-component';
|
import ProjectSelectComponent from './project-select-component';
|
||||||
import { fetchProjects } from './../../store/project/actions';
|
import { fetchProjects } from './../../store/project/actions';
|
||||||
|
import { P } from '../common/flags';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => ({
|
||||||
const projects = state.projects.toJS();
|
projects: state.projects.toJS(),
|
||||||
|
enabled: !!state.uiConfig.toJS().flags[P],
|
||||||
return {
|
});
|
||||||
projects,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProjectContainer = connect(mapStateToProps, { fetchProjects })(ProjectSelectComponent);
|
const ProjectContainer = connect(mapStateToProps, { fetchProjects })(ProjectSelectComponent);
|
||||||
|
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import StrategyConstraintInput from './strategy-constraint-input';
|
||||||
|
import { C } from '../../../common/flags';
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
state => ({
|
||||||
|
contextNames: state.context.toJS().map(c => c.name),
|
||||||
|
contextFields: state.context.toJS(),
|
||||||
|
enabled: !!state.uiConfig.toJS().flags[C],
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)(StrategyConstraintInput);
|
@ -0,0 +1,128 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { IconButton } from 'react-mdl';
|
||||||
|
import Select from 'react-select';
|
||||||
|
import MySelect from '../../../common/select';
|
||||||
|
import InputListField from '../../../common/input-list-field';
|
||||||
|
import { selectStyles } from '../../../common';
|
||||||
|
|
||||||
|
const constraintOperators = [
|
||||||
|
{ key: 'IN', label: 'IN' },
|
||||||
|
{ key: 'NOT_IN', label: 'NOT_IN' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default class StrategyConstraintInputField extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
constraint: PropTypes.object.isRequired,
|
||||||
|
updateConstraint: PropTypes.func.isRequired,
|
||||||
|
removeConstraint: PropTypes.func.isRequired,
|
||||||
|
contextFields: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = { error: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const { constraint, updateConstraint } = this.props;
|
||||||
|
const values = constraint.values;
|
||||||
|
const filtered = values.filter(v => v).map(v => v.trim());
|
||||||
|
if (filtered.length !== values.length) {
|
||||||
|
updateConstraint(filtered, 'values');
|
||||||
|
}
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
this.setState({ error: 'You need to specify at least one value' });
|
||||||
|
} else {
|
||||||
|
this.setState({ error: undefined });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateConstraintValues = evt => {
|
||||||
|
const { updateConstraint } = this.props;
|
||||||
|
const values = evt.target.value.split(/,\s?/);
|
||||||
|
const trimmedValues = values.map(v => v.trim());
|
||||||
|
updateConstraint(trimmedValues, 'values');
|
||||||
|
};
|
||||||
|
|
||||||
|
handleKeyDownConstraintValues = evt => {
|
||||||
|
const { updateConstraint } = this.props;
|
||||||
|
|
||||||
|
if (evt.key === 'Backspace') {
|
||||||
|
const currentValue = evt.target.value;
|
||||||
|
if (currentValue.endsWith(', ')) {
|
||||||
|
evt.preventDefault();
|
||||||
|
const value = currentValue.slice(0, -2);
|
||||||
|
updateConstraint(value.split(/,\s*/), 'values');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChangeValue = selectedOptions => {
|
||||||
|
const { updateConstraint } = this.props;
|
||||||
|
const values = selectedOptions ? selectedOptions.map(o => o.value) : [];
|
||||||
|
updateConstraint(values, 'values');
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { contextFields, constraint, removeConstraint, updateConstraint } = this.props;
|
||||||
|
const constraintContextNames = contextFields.map(f => ({ key: f.name, label: f.name }));
|
||||||
|
const constraintDef = contextFields.find(c => c.name === constraint.contextName);
|
||||||
|
|
||||||
|
const options =
|
||||||
|
constraintDef && constraintDef.legalValues && constraintDef.legalValues.length > 0
|
||||||
|
? constraintDef.legalValues.map(l => ({ value: l, label: l }))
|
||||||
|
: undefined;
|
||||||
|
const values = constraint.values.map(v => ({ value: v, label: v }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<MySelect
|
||||||
|
name="contextName"
|
||||||
|
label="Context Field"
|
||||||
|
options={constraintContextNames}
|
||||||
|
value={constraint.contextName}
|
||||||
|
onChange={evt => updateConstraint(evt.target.value, 'contextName')}
|
||||||
|
style={{ width: 'auto' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<MySelect
|
||||||
|
name="operator"
|
||||||
|
label="Operator"
|
||||||
|
options={constraintOperators}
|
||||||
|
value={constraint.operator}
|
||||||
|
onChange={evt => updateConstraint(evt.target.value, 'operator')}
|
||||||
|
style={{ width: 'auto' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td style={{ width: '100%' }}>
|
||||||
|
{options ? (
|
||||||
|
<Select
|
||||||
|
styles={selectStyles}
|
||||||
|
value={values}
|
||||||
|
options={options}
|
||||||
|
isMulti
|
||||||
|
onChange={this.handleChangeValue}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<InputListField
|
||||||
|
name="values"
|
||||||
|
error={this.state.error}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
values={constraint.values}
|
||||||
|
label="Values (v1, v2, v3)"
|
||||||
|
updateValues={values => updateConstraint(values, 'values')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<IconButton name="delete" onClick={removeConstraint} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Tooltip, Icon } from 'react-mdl';
|
||||||
|
import StrategyConstraintInputField from './strategy-constraint-input-field';
|
||||||
|
|
||||||
|
export default class StrategyConstraintInput extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
constraints: PropTypes.array.isRequired,
|
||||||
|
updateConstraints: PropTypes.func.isRequired,
|
||||||
|
contextNames: PropTypes.array.isRequired,
|
||||||
|
contextFields: PropTypes.array.isRequired,
|
||||||
|
enabled: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = { errors: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
addConstraint = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const { constraints, updateConstraints, contextNames } = this.props;
|
||||||
|
|
||||||
|
const updatedConstraints = [...constraints];
|
||||||
|
updatedConstraints.push({ contextName: contextNames[0], operator: 'IN', values: [] });
|
||||||
|
|
||||||
|
updateConstraints(updatedConstraints);
|
||||||
|
};
|
||||||
|
|
||||||
|
removeConstraint = (index, evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const { constraints, updateConstraints } = this.props;
|
||||||
|
|
||||||
|
const updatedConstraints = [...constraints];
|
||||||
|
updatedConstraints.splice(index, 1);
|
||||||
|
|
||||||
|
updateConstraints(updatedConstraints);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateConstraint = (index, value, field) => {
|
||||||
|
const { constraints } = this.props;
|
||||||
|
|
||||||
|
// TOOD: value should be array
|
||||||
|
const updatedConstraints = [...constraints];
|
||||||
|
const constraint = updatedConstraints[index];
|
||||||
|
|
||||||
|
constraint[field] = value;
|
||||||
|
|
||||||
|
this.props.updateConstraints(updatedConstraints);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { constraints, contextFields, enabled } = this.props;
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<strong>{'Constraints '}</strong>
|
||||||
|
<Tooltip label={<span>Use context fields to constrain the activation strategy.</span>}>
|
||||||
|
<Icon name="info" style={{ fontSize: '0.9em', color: 'gray' }} />
|
||||||
|
</Tooltip>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{constraints.map((c, index) => (
|
||||||
|
<StrategyConstraintInputField
|
||||||
|
key={`${c.contextName}-${index}`}
|
||||||
|
id={`${c.contextName}-${index}`}
|
||||||
|
constraint={c}
|
||||||
|
contextFields={contextFields}
|
||||||
|
updateConstraint={this.updateConstraint.bind(this, index)}
|
||||||
|
removeConstraint={this.removeConstraint.bind(this, index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
<a href="#add-constraint" title="Add constraint" onClick={this.addConstraint}>
|
||||||
|
Add constraint
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,8 @@ import LoadingStrategy from './loading-strategy';
|
|||||||
|
|
||||||
import styles from './strategy.module.scss';
|
import styles from './strategy.module.scss';
|
||||||
|
|
||||||
|
import StrategyConstraints from './constraint/strategy-constraint-input-container';
|
||||||
|
|
||||||
export default class StrategyConfigureComponent extends React.Component {
|
export default class StrategyConfigureComponent extends React.Component {
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -113,6 +115,10 @@ export default class StrategyConfigureComponent extends React.Component {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
||||||
<CardText style={{ width: 'unset' }}>
|
<CardText style={{ width: 'unset' }}>
|
||||||
|
<StrategyConstraints
|
||||||
|
updateConstraints={this.updateConstraints}
|
||||||
|
constraints={strategy.constraints || []}
|
||||||
|
/>
|
||||||
<InputType
|
<InputType
|
||||||
parameters={parameters}
|
parameters={parameters}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
|
@ -7,6 +7,7 @@ import { UPDATE_FEATURE } from '../../../../permissions';
|
|||||||
import { weightTypes } from '../enums';
|
import { weightTypes } from '../enums';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
jest.mock('react-mdl');
|
||||||
|
jest.mock('../e-override-config', () => 'OverrideConfig');
|
||||||
|
|
||||||
test('renders correctly with without variants', () => {
|
test('renders correctly with without variants', () => {
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
|
@ -6,7 +6,7 @@ import styles from './variant.module.scss';
|
|||||||
import MySelect from '../../common/select';
|
import MySelect from '../../common/select';
|
||||||
import { trim, modalStyles } from '../../common/util';
|
import { trim, modalStyles } from '../../common/util';
|
||||||
import { weightTypes } from './enums';
|
import { weightTypes } from './enums';
|
||||||
import OverrideConfig from './override-config';
|
import OverrideConfig from './e-override-config';
|
||||||
|
|
||||||
Modal.setAppElement('#app');
|
Modal.setAppElement('#app');
|
||||||
|
|
||||||
|
81
frontend/src/component/feature/variant/e-override-config.jsx
Normal file
81
frontend/src/component/feature/variant/e-override-config.jsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Grid, Cell, IconButton } from 'react-mdl';
|
||||||
|
import Select from 'react-select';
|
||||||
|
import MySelect from '../../common/select';
|
||||||
|
import InputListField from '../../common/input-list-field';
|
||||||
|
import { selectStyles } from '../../common';
|
||||||
|
|
||||||
|
function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, removeOverride, contextDefinitions }) {
|
||||||
|
const contextNames = contextDefinitions.map(c => ({ key: c.name, label: c.name }));
|
||||||
|
|
||||||
|
const updateValues = i => values => {
|
||||||
|
updateOverrideValues(i, values);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSelectValues = i => values => {
|
||||||
|
updateOverrideValues(i, values ? values.map(v => v.value) : undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapSelectValues = (values = []) => values.map(v => ({ label: v, value: v }));
|
||||||
|
|
||||||
|
return overrides.map((o, i) => {
|
||||||
|
const legalValues = contextDefinitions.find(c => c.name === o.contextName).legalValues || [];
|
||||||
|
const options = legalValues.map(v => ({ value: v, label: v, key: v }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid noSpacing key={`override=${i}`}>
|
||||||
|
<Cell col={3}>
|
||||||
|
<MySelect
|
||||||
|
name="contextName"
|
||||||
|
label="Context Field"
|
||||||
|
value={o.contextName}
|
||||||
|
options={contextNames}
|
||||||
|
onChange={updateOverrideType(i)}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={8}>
|
||||||
|
{legalValues && legalValues.length > 0 ? (
|
||||||
|
<div style={{ paddingTop: '12px' }}>
|
||||||
|
<Select
|
||||||
|
key={`override-select=${i}`}
|
||||||
|
styles={selectStyles}
|
||||||
|
value={mapSelectValues(o.values)}
|
||||||
|
options={options}
|
||||||
|
isMulti
|
||||||
|
onChange={updateSelectValues(i)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<InputListField
|
||||||
|
label="Values (v1, v2, ...)"
|
||||||
|
name="values"
|
||||||
|
placeholder=""
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
values={o.values}
|
||||||
|
updateValues={updateValues(i)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Cell>
|
||||||
|
<Cell col={1} style={{ textAlign: 'right', paddingTop: '12px' }}>
|
||||||
|
<IconButton name="delete" onClick={removeOverride(i)} />
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
OverrideConfig.propTypes = {
|
||||||
|
overrides: PropTypes.array.isRequired,
|
||||||
|
updateOverrideType: PropTypes.func.isRequired,
|
||||||
|
updateOverrideValues: PropTypes.func.isRequired,
|
||||||
|
removeOverride: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
contextDefinitions: state.context.toJS(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, {})(OverrideConfig);
|
@ -43,6 +43,20 @@ exports[`should render DrawerMenu 1`] = `
|
|||||||
>
|
>
|
||||||
Applications
|
Applications
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
aria-current={null}
|
||||||
|
href="/context"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Context Fields
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
aria-current={null}
|
||||||
|
href="/projects"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
aria-current={null}
|
aria-current={null}
|
||||||
href="/tag-types"
|
href="/tag-types"
|
||||||
@ -161,6 +175,20 @@ exports[`should render DrawerMenu with "features" selected 1`] = `
|
|||||||
>
|
>
|
||||||
Applications
|
Applications
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
aria-current={null}
|
||||||
|
href="/context"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Context Fields
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
aria-current={null}
|
||||||
|
href="/projects"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
aria-current={null}
|
aria-current={null}
|
||||||
href="/tag-types"
|
href="/tag-types"
|
||||||
|
@ -32,6 +32,20 @@ Array [
|
|||||||
"path": "/applications",
|
"path": "/applications",
|
||||||
"title": "Applications",
|
"title": "Applications",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"flag": "C",
|
||||||
|
"icon": "album",
|
||||||
|
"path": "/context",
|
||||||
|
"title": "Context Fields",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"flag": "P",
|
||||||
|
"icon": "folder_open",
|
||||||
|
"path": "/projects",
|
||||||
|
"title": "Projects",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"icon": "label",
|
"icon": "label",
|
||||||
@ -148,8 +162,8 @@ Array [
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"hidden": true,
|
"flag": "C",
|
||||||
"icon": "apps",
|
"icon": "album",
|
||||||
"path": "/context",
|
"path": "/context",
|
||||||
"title": "Context Fields",
|
"title": "Context Fields",
|
||||||
},
|
},
|
||||||
@ -167,11 +181,36 @@ Array [
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"hidden": true,
|
"flag": "P",
|
||||||
"icon": "folder_open",
|
"icon": "folder_open",
|
||||||
"path": "/projects",
|
"path": "/projects",
|
||||||
"title": "Projects",
|
"title": "Projects",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"parent": "/admin",
|
||||||
|
"path": "/admin/api",
|
||||||
|
"title": "API access",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"parent": "/admin",
|
||||||
|
"path": "/admin/users",
|
||||||
|
"title": "Users",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"parent": "/admin",
|
||||||
|
"path": "/admin/auth",
|
||||||
|
"title": "Authentication",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"hidden": true,
|
||||||
|
"icon": "album",
|
||||||
|
"path": "/admin",
|
||||||
|
"title": "Admin",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"parent": "/tag-types",
|
"parent": "/tag-types",
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { routes, baseRoutes, getRoute } from '../routes';
|
import { routes, baseRoutes, getRoute } from '../routes';
|
||||||
|
|
||||||
test('returns all defined routes', () => {
|
test('returns all defined routes', () => {
|
||||||
expect(routes.length).toEqual(28);
|
expect(routes.length).toEqual(32);
|
||||||
expect(routes).toMatchSnapshot();
|
expect(routes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns all baseRoutes', () => {
|
test('returns all baseRoutes', () => {
|
||||||
expect(baseRoutes.length).toEqual(8);
|
expect(baseRoutes.length).toEqual(10);
|
||||||
expect(baseRoutes).toMatchSnapshot();
|
expect(baseRoutes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,6 +6,13 @@ import styles from '../styles.module.scss';
|
|||||||
|
|
||||||
import { baseRoutes as routes } from './routes';
|
import { baseRoutes as routes } from './routes';
|
||||||
|
|
||||||
|
const filterByFlags = flags => r => {
|
||||||
|
if (r.flag && !flags[r.flag]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
function getIcon(name) {
|
function getIcon(name) {
|
||||||
if (name === 'c_github') {
|
if (name === 'c_github') {
|
||||||
return <i className={['material-icons', styles.navigationIcon, styles.iconGitHub].join(' ')} />;
|
return <i className={['material-icons', styles.navigationIcon, styles.iconGitHub].join(' ')} />;
|
||||||
@ -41,7 +48,7 @@ function renderLink(link) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DrawerMenu = ({ links = [], title = 'Unleash' }) => (
|
export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {} }) => (
|
||||||
<Drawer style={{ boxShadow: 'none', border: 0 }}>
|
<Drawer style={{ boxShadow: 'none', border: 0 }}>
|
||||||
<span className={[styles.drawerTitle, 'mdl-layout-title'].join(' ')}>
|
<span className={[styles.drawerTitle, 'mdl-layout-title'].join(' ')}>
|
||||||
<img src="public/logo.png" width="32" height="32" className={styles.drawerTitleLogo} />
|
<img src="public/logo.png" width="32" height="32" className={styles.drawerTitleLogo} />
|
||||||
@ -49,7 +56,7 @@ export const DrawerMenu = ({ links = [], title = 'Unleash' }) => (
|
|||||||
</span>
|
</span>
|
||||||
<hr />
|
<hr />
|
||||||
<Navigation className={styles.navigation}>
|
<Navigation className={styles.navigation}>
|
||||||
{routes.map(item => (
|
{routes.filter(filterByFlags(flags)).map(item => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
@ -70,4 +77,5 @@ export const DrawerMenu = ({ links = [], title = 'Unleash' }) => (
|
|||||||
DrawerMenu.propTypes = {
|
DrawerMenu.propTypes = {
|
||||||
links: PropTypes.array,
|
links: PropTypes.array,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
flags: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
@ -6,17 +6,18 @@ import { Route } from 'react-router-dom';
|
|||||||
import { DrawerMenu } from './drawer';
|
import { DrawerMenu } from './drawer';
|
||||||
import Breadcrum from './breadcrumb';
|
import Breadcrum from './breadcrumb';
|
||||||
import ShowUserContainer from '../user/show-user-container';
|
import ShowUserContainer from '../user/show-user-container';
|
||||||
import { loadInitalData } from './../../store/loader';
|
import { loadInitialData } from './../../store/loader';
|
||||||
|
|
||||||
class HeaderComponent extends PureComponent {
|
class HeaderComponent extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
uiConfig: PropTypes.object.isRequired,
|
uiConfig: PropTypes.object.isRequired,
|
||||||
loadInitalData: PropTypes.func.isRequired,
|
init: PropTypes.func.isRequired,
|
||||||
location: PropTypes.object.isRequired,
|
location: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadInitalData();
|
const { init, uiConfig } = this.props;
|
||||||
|
init(uiConfig.flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
@ -35,7 +36,7 @@ class HeaderComponent extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { headerBackground, links, name } = this.props.uiConfig;
|
const { headerBackground, links, name, flags } = this.props.uiConfig;
|
||||||
const style = headerBackground ? { background: headerBackground } : {};
|
const style = headerBackground ? { background: headerBackground } : {};
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@ -44,10 +45,10 @@ class HeaderComponent extends PureComponent {
|
|||||||
<ShowUserContainer />
|
<ShowUserContainer />
|
||||||
</Navigation>
|
</Navigation>
|
||||||
</Header>
|
</Header>
|
||||||
<DrawerMenu links={links} title={name} />
|
<DrawerMenu links={links} title={name} flags={flags} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(state => ({ uiConfig: state.uiConfig.toJS() }), { loadInitalData })(HeaderComponent);
|
export default connect(state => ({ uiConfig: state.uiConfig.toJS() }), { init: loadInitialData })(HeaderComponent);
|
||||||
|
@ -26,6 +26,12 @@ import CreateTag from '../../page/tags/create';
|
|||||||
import Addons from '../../page/addons';
|
import Addons from '../../page/addons';
|
||||||
import AddonsCreate from '../../page/addons/create';
|
import AddonsCreate from '../../page/addons/create';
|
||||||
import AddonsEdit from '../../page/addons/edit';
|
import AddonsEdit from '../../page/addons/edit';
|
||||||
|
import Admin from '../../page/admin';
|
||||||
|
import AdminApi from '../../page/admin/api';
|
||||||
|
import AdminUsers from '../../page/admin/users';
|
||||||
|
import AdminAuth from '../../page/admin/auth';
|
||||||
|
import { P, C } from '../common/flags';
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Features
|
// Features
|
||||||
{ path: '/features/create', parent: '/features', title: 'Create', component: CreateFeatureToggle },
|
{ path: '/features/create', parent: '/features', title: 'Create', component: CreateFeatureToggle },
|
||||||
@ -58,12 +64,18 @@ export const routes = [
|
|||||||
// Context
|
// Context
|
||||||
{ path: '/context/create', parent: '/context', title: 'Create', component: CreateContextField },
|
{ path: '/context/create', parent: '/context', title: 'Create', component: CreateContextField },
|
||||||
{ path: '/context/edit/:name', parent: '/context', title: ':name', component: EditContextField },
|
{ path: '/context/edit/:name', parent: '/context', title: ':name', component: EditContextField },
|
||||||
{ path: '/context', title: 'Context Fields', icon: 'apps', component: ContextFields, hidden: true },
|
{ path: '/context', title: 'Context Fields', icon: 'album', component: ContextFields, flag: C },
|
||||||
|
|
||||||
// Project
|
// Project
|
||||||
{ path: '/projects/create', parent: '/projects', title: 'Create', component: CreateProject },
|
{ path: '/projects/create', parent: '/projects', title: 'Create', component: CreateProject },
|
||||||
{ path: '/projects/edit/:id', parent: '/projects', title: ':id', component: EditProject },
|
{ path: '/projects/edit/:id', parent: '/projects', title: ':id', component: EditProject },
|
||||||
{ path: '/projects', title: 'Projects', icon: 'folder_open', component: ListProjects, hidden: true },
|
{ path: '/projects', title: 'Projects', icon: 'folder_open', component: ListProjects, flag: P },
|
||||||
|
|
||||||
|
// Admin
|
||||||
|
{ path: '/admin/api', parent: '/admin', title: 'API access', component: AdminApi },
|
||||||
|
{ path: '/admin/users', parent: '/admin', title: 'Users', component: AdminUsers },
|
||||||
|
{ path: '/admin/auth', parent: '/admin', title: 'Authentication', component: AdminAuth },
|
||||||
|
{ path: '/admin', title: 'Admin', icon: 'album', component: Admin, hidden: true },
|
||||||
|
|
||||||
{ path: '/tag-types/create', parent: '/tag-types', title: 'Create', component: CreateTagType },
|
{ path: '/tag-types/create', parent: '/tag-types', title: 'Create', component: CreateTagType },
|
||||||
{ path: '/tag-types/edit/:name', parent: '/tag-types', title: ':name', component: EditTagType },
|
{ path: '/tag-types/edit/:name', parent: '/tag-types', title: ':name', component: EditTagType },
|
||||||
|
@ -37,7 +37,7 @@ class AuthComponent extends React.Component {
|
|||||||
user: PropTypes.object.isRequired,
|
user: PropTypes.object.isRequired,
|
||||||
unsecureLogin: PropTypes.func.isRequired,
|
unsecureLogin: PropTypes.func.isRequired,
|
||||||
passwordLogin: PropTypes.func.isRequired,
|
passwordLogin: PropTypes.func.isRequired,
|
||||||
loadInitalData: PropTypes.func.isRequired,
|
loadInitialData: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ class AuthComponent extends React.Component {
|
|||||||
<AuthenticationPasswordComponent
|
<AuthenticationPasswordComponent
|
||||||
passwordLogin={this.props.passwordLogin}
|
passwordLogin={this.props.passwordLogin}
|
||||||
authDetails={authDetails}
|
authDetails={authDetails}
|
||||||
loadInitalData={this.props.loadInitalData}
|
loadInitialData={this.props.loadInitialData}
|
||||||
history={this.props.history}
|
history={this.props.history}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -60,7 +60,7 @@ class AuthComponent extends React.Component {
|
|||||||
<AuthenticationSimpleComponent
|
<AuthenticationSimpleComponent
|
||||||
unsecureLogin={this.props.unsecureLogin}
|
unsecureLogin={this.props.unsecureLogin}
|
||||||
authDetails={authDetails}
|
authDetails={authDetails}
|
||||||
loadInitalData={this.props.loadInitalData}
|
loadInitialData={this.props.loadInitialData}
|
||||||
history={this.props.history}
|
history={this.props.history}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import AuthenticationComponent from './authentication-component';
|
import AuthenticationComponent from './authentication-component';
|
||||||
import { unsecureLogin, passwordLogin } from '../../store/user/actions';
|
import { insecureLogin, passwordLogin } from '../../store/user/actions';
|
||||||
import { loadInitalData } from './../../store/loader';
|
import { loadInitialData } from './../../store/loader';
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = (dispatch, props) => ({
|
||||||
unsecureLogin,
|
insecureLogin: (path, user) => insecureLogin(path, user)(dispatch),
|
||||||
passwordLogin,
|
passwordLogin: (path, user) => passwordLogin(path, user)(dispatch),
|
||||||
loadInitalData,
|
loadInitialData: () => loadInitialData(props.flags)(dispatch),
|
||||||
};
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
user: state.user.toJS(),
|
user: state.user.toJS(),
|
||||||
|
flags: state.uiConfig.toJS().flags,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(AuthenticationComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(AuthenticationComponent);
|
||||||
|
@ -6,7 +6,7 @@ class EnterpriseAuthenticationComponent extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
authDetails: PropTypes.object.isRequired,
|
authDetails: PropTypes.object.isRequired,
|
||||||
passwordLogin: PropTypes.func.isRequired,
|
passwordLogin: PropTypes.func.isRequired,
|
||||||
loadInitalData: PropTypes.func.isRequired,
|
loadInitialData: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ class EnterpriseAuthenticationComponent extends React.Component {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.props.passwordLogin(path, user);
|
await this.props.passwordLogin(path, user);
|
||||||
await this.props.loadInitalData();
|
await this.props.loadInitialData();
|
||||||
this.props.history.push(`/`);
|
this.props.history.push(`/`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.statusCode === 404) {
|
if (error.statusCode === 404) {
|
||||||
|
@ -6,7 +6,7 @@ class SimpleAuthenticationComponent extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
authDetails: PropTypes.object.isRequired,
|
authDetails: PropTypes.object.isRequired,
|
||||||
unsecureLogin: PropTypes.func.isRequired,
|
unsecureLogin: PropTypes.func.isRequired,
|
||||||
loadInitalData: PropTypes.func.isRequired,
|
loadInitialData: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ class SimpleAuthenticationComponent extends React.Component {
|
|||||||
|
|
||||||
this.props
|
this.props
|
||||||
.unsecureLogin(path, user)
|
.unsecureLogin(path, user)
|
||||||
.then(this.props.loadInitalData)
|
.then(this.props.loadInitialData)
|
||||||
.then(() => this.props.history.push(`/`));
|
.then(() => this.props.history.push(`/`));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
13
frontend/src/page/admin/admin-menu.jsx
Normal file
13
frontend/src/page/admin/admin-menu.jsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function AdminMenu() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link to="/admin/users">Users</Link> | <Link to="/admin/api">API Access</Link> |{' '}
|
||||||
|
<Link to="/admin/auth">Authentication</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminMenu;
|
25
frontend/src/page/admin/api/api-howto.jsx
Normal file
25
frontend/src/page/admin/api/api-howto.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function ApiHowTo() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#cfe5ff',
|
||||||
|
border: '2px solid #c4e1ff',
|
||||||
|
padding: '8px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Read the{' '}
|
||||||
|
<a href="https://www.unleash-hosted.com/docs" target="_blank">
|
||||||
|
Getting started guide
|
||||||
|
</a>{' '}
|
||||||
|
to learn how to connect to the Unleash API form your application or programmatically. <br /> <br />
|
||||||
|
Please note it can take up to 1 minute before a new API key is activated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiHowTo;
|
64
frontend/src/page/admin/api/api-key-create.jsx
Normal file
64
frontend/src/page/admin/api/api-key-create.jsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Textfield, Button } from 'react-mdl';
|
||||||
|
|
||||||
|
function CreateApiKey({ addKey }) {
|
||||||
|
const [type, setType] = useState('CLIENT');
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const [username, setUsername] = useState();
|
||||||
|
const [error, setError] = useState();
|
||||||
|
|
||||||
|
const toggle = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
setShow(!show);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!username) {
|
||||||
|
setError('You must define a username');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await addKey({ username, type });
|
||||||
|
setUsername('');
|
||||||
|
setType('CLIENT');
|
||||||
|
setShow(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ margin: '5px' }}>
|
||||||
|
{show ? (
|
||||||
|
<form onSubmit={submit}>
|
||||||
|
<Textfield
|
||||||
|
value={username}
|
||||||
|
name="username"
|
||||||
|
onChange={e => setUsername(e.target.value)}
|
||||||
|
label="Username"
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '200px' }}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<select value={type} onChange={e => setType(e.target.value)}>
|
||||||
|
<option value="CLIENT">Client</option>
|
||||||
|
<option value="ADMIN">Admin</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<Button primary mini="true" type="submit">
|
||||||
|
Create new key
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<a href="" onClick={toggle}>
|
||||||
|
Add new access key
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateApiKey.propTypes = {
|
||||||
|
addKey: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateApiKey;
|
14
frontend/src/page/admin/api/api-key-list-container.js
Normal file
14
frontend/src/page/admin/api/api-key-list-container.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import Component from './api-key-list';
|
||||||
|
import { fetchApiKeys, removeKey, addKey } from './../../../store/e-api-admin/actions';
|
||||||
|
import { hasPermission } from '../../../permissions';
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
state => ({
|
||||||
|
location: state.settings.toJS().location || {},
|
||||||
|
keys: state.apiAdmin.toJS(),
|
||||||
|
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
|
||||||
|
}),
|
||||||
|
{ fetchApiKeys, removeKey, addKey }
|
||||||
|
)(Component);
|
89
frontend/src/page/admin/api/api-key-list.jsx
Normal file
89
frontend/src/page/admin/api/api-key-list.jsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/* eslint-disable no-alert */
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon } from 'react-mdl';
|
||||||
|
import { formatFullDateTimeWithLocale } from '../../../component/common/util';
|
||||||
|
import CreateApiKey from './api-key-create';
|
||||||
|
import Secret from './secret';
|
||||||
|
import ApiHowTo from './api-howto';
|
||||||
|
|
||||||
|
function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermission }) {
|
||||||
|
const deleteKey = async key => {
|
||||||
|
const shouldDelte = confirm('Are you sure?');
|
||||||
|
if (shouldDelte) {
|
||||||
|
await removeKey(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchApiKeys();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ApiHowTo />
|
||||||
|
<table className="mdl-data-table mdl-shadow--2dp">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric" width="20" style={{ textAlign: 'left' }}>
|
||||||
|
Created
|
||||||
|
</th>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric" width="20" style={{ textAlign: 'left' }}>
|
||||||
|
Username
|
||||||
|
</th>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric" width="10" style={{ textAlign: 'left' }}>
|
||||||
|
Acess Type
|
||||||
|
</th>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric" style={{ textAlign: 'left' }}>
|
||||||
|
Secret
|
||||||
|
</th>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric" width="10" style={{ textAlign: 'lerightft' }}>
|
||||||
|
Action
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{keys.map(item => (
|
||||||
|
<tr key={item.key}>
|
||||||
|
<td style={{ textAlign: 'left' }}>
|
||||||
|
{formatFullDateTimeWithLocale(item.created, location.locale)}
|
||||||
|
</td>
|
||||||
|
<td style={{ textAlign: 'left' }}>{item.username}</td>
|
||||||
|
<td style={{ textAlign: 'left' }}>{item.priviliges[0]}</td>
|
||||||
|
<td style={{ textAlign: 'left' }}>
|
||||||
|
<Secret value={item.key} />
|
||||||
|
</td>
|
||||||
|
{hasPermission('ADMIN') ? (
|
||||||
|
<td style={{ textAlign: 'right' }}>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
deleteKey(item.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="delete" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
) : (
|
||||||
|
<td />
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{hasPermission('ADMIN') ? <CreateApiKey addKey={addKey} /> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiKeyList.propTypes = {
|
||||||
|
location: PropTypes.object,
|
||||||
|
fetchApiKeys: PropTypes.func.isRequired,
|
||||||
|
removeKey: PropTypes.func.isRequired,
|
||||||
|
addKey: PropTypes.func.isRequired,
|
||||||
|
keys: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiKeyList;
|
20
frontend/src/page/admin/api/index.js
Normal file
20
frontend/src/page/admin/api/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ApiKeyList from './api-key-list-container';
|
||||||
|
|
||||||
|
import AdminMenu from '../admin-menu';
|
||||||
|
|
||||||
|
const render = () => (
|
||||||
|
<div>
|
||||||
|
<AdminMenu />
|
||||||
|
<h3>API Access</h3>
|
||||||
|
<ApiKeyList />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
match: PropTypes.object.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
31
frontend/src/page/admin/api/secret.jsx
Normal file
31
frontend/src/page/admin/api/secret.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon } from 'react-mdl';
|
||||||
|
|
||||||
|
function Secret({ value }) {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const toggle = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
setShow(!show);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{show ? (
|
||||||
|
<input readOnly value={value} style={{ width: '240px' }} />
|
||||||
|
) : (
|
||||||
|
<span>***************************</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<a href="" onClick={toggle} title="Show token">
|
||||||
|
<Icon style={{ marginLeft: '5px', fontSize: '1.2em' }} name="visibility" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Secret.propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Secret;
|
13
frontend/src/page/admin/auth/google-auth-container.js
Normal file
13
frontend/src/page/admin/auth/google-auth-container.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import GoogleAuth from './google-auth';
|
||||||
|
import { getGoogleConfig, updateGoogleConfig } from './../../../store/e-admin-auth/actions';
|
||||||
|
import { hasPermission } from '../../../permissions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
config: state.authAdmin.get('google'),
|
||||||
|
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Container = connect(mapStateToProps, { getGoogleConfig, updateGoogleConfig })(GoogleAuth);
|
||||||
|
|
||||||
|
export default Container;
|
191
frontend/src/page/admin/auth/google-auth.jsx
Normal file
191
frontend/src/page/admin/auth/google-auth.jsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, Grid, Cell, Switch, Textfield } from 'react-mdl';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
enabled: false,
|
||||||
|
autoCreate: false,
|
||||||
|
unleashHostname: location.hostname,
|
||||||
|
};
|
||||||
|
|
||||||
|
function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission }) {
|
||||||
|
const [data, setData] = useState(initialState);
|
||||||
|
const [info, setInfo] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getGoogleConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (config.clientId) {
|
||||||
|
setData(config);
|
||||||
|
}
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
if (!hasPermission('ADMIN')) {
|
||||||
|
return <span>You need admin privileges to access this section.</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateField = e => {
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateEnabled = () => {
|
||||||
|
setData({ ...data, enabled: !data.enabled });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAutoCreate = () => {
|
||||||
|
setData({ ...data, autoCreate: !data.autoCreate });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setInfo('...saving');
|
||||||
|
try {
|
||||||
|
await updateGoogleConfig(data);
|
||||||
|
setInfo('Settings stored');
|
||||||
|
setTimeout(() => setInfo(''), 2000);
|
||||||
|
} catch (e) {
|
||||||
|
setInfo(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Grid style={{ background: '#EFEFEF' }}>
|
||||||
|
<Cell col={12}>
|
||||||
|
<p>
|
||||||
|
Please read the{' '}
|
||||||
|
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication/google" target="_blank">
|
||||||
|
documentation
|
||||||
|
</a>{' '}
|
||||||
|
to learn how to integrate with Google OAuth 2.0. <br />
|
||||||
|
<br />
|
||||||
|
Callback URL: <code>https://[unleash.hostname.com]/auth/google/callback</code>
|
||||||
|
</p>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Enable</strong>
|
||||||
|
<p>
|
||||||
|
Enable Google users to login. Value is ignored if Client ID and Client Secret are not
|
||||||
|
defined.
|
||||||
|
</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6} style={{ padding: '20px' }}>
|
||||||
|
<Switch onChange={updateEnabled} name="enabled" checked={data.enabled}>
|
||||||
|
{data.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</Switch>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Client ID</strong>
|
||||||
|
<p>(Required) The Client ID provided by Google when registering the application.</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6}>
|
||||||
|
<Textfield
|
||||||
|
onChange={updateField}
|
||||||
|
label="Client ID"
|
||||||
|
name="clientId"
|
||||||
|
placeholder=""
|
||||||
|
value={data.clientId}
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Client Secret</strong>
|
||||||
|
<p>(Required) Client Secret provided by Google when registering the application.</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6}>
|
||||||
|
<Textfield
|
||||||
|
onChange={updateField}
|
||||||
|
label="Client Secret"
|
||||||
|
name="clientSecret"
|
||||||
|
value={data.clientSecret}
|
||||||
|
placeholder=""
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Unleash hostname</strong>
|
||||||
|
<p>
|
||||||
|
(Required) The hostname you are running Unleash on that Google should send the user back to.
|
||||||
|
The final callback URL will be{' '}
|
||||||
|
<small>
|
||||||
|
<code>https://[unleash.hostname.com]/auth/google/callback</code>
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6}>
|
||||||
|
<Textfield
|
||||||
|
onChange={updateField}
|
||||||
|
label="Unleash Hostname"
|
||||||
|
name="unleashHostname"
|
||||||
|
placeholder=""
|
||||||
|
value={data.unleashHostname}
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Auto-create users</strong>
|
||||||
|
<p>Enable automatic creation of new users when signing in with Google.</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6} style={{ padding: '20px' }}>
|
||||||
|
<Switch onChange={updateAutoCreate} name="enabled" checked={data.autoCreate}>
|
||||||
|
Auto-create users
|
||||||
|
</Switch>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Email domains</strong>
|
||||||
|
<p>(Optional) Comma separated list of email domains that should be allowed to sign in.</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6}>
|
||||||
|
<Textfield
|
||||||
|
onChange={updateField}
|
||||||
|
label="Email domains"
|
||||||
|
name="emailDomains"
|
||||||
|
value={data.emailDomains}
|
||||||
|
placeholder="@company.com, @anotherCompany.com"
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<Button raised accent type="submit">
|
||||||
|
Save
|
||||||
|
</Button>{' '}
|
||||||
|
<small>{info}</small>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GoogleAuth.propTypes = {
|
||||||
|
config: PropTypes.object,
|
||||||
|
getGoogleConfig: PropTypes.func.isRequired,
|
||||||
|
updateGoogleConfig: PropTypes.func.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GoogleAuth;
|
31
frontend/src/page/admin/auth/index.js
Normal file
31
frontend/src/page/admin/auth/index.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Tabs, Tab } from 'react-mdl';
|
||||||
|
import AdminMenu from '../admin-menu';
|
||||||
|
import GoogleAuth from './google-auth-container';
|
||||||
|
import SamlAuth from './saml-auth-container';
|
||||||
|
|
||||||
|
function AdminAuthPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AdminMenu />
|
||||||
|
<h3>Authentication</h3>
|
||||||
|
<div className="demo-tabs">
|
||||||
|
<Tabs activeTab={activeTab} onChange={setActiveTab} ripple>
|
||||||
|
<Tab>SAML 2.0</Tab>
|
||||||
|
<Tab>Google</Tab>
|
||||||
|
</Tabs>
|
||||||
|
<section>{activeTab === 0 ? <SamlAuth /> : <GoogleAuth />}</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminAuthPage.propTypes = {
|
||||||
|
match: PropTypes.object.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminAuthPage;
|
13
frontend/src/page/admin/auth/saml-auth-container.js
Normal file
13
frontend/src/page/admin/auth/saml-auth-container.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import SamlAuth from './saml-auth';
|
||||||
|
import { getSamlConfig, updateSamlConfig } from './../../../store/e-admin-auth/actions';
|
||||||
|
import { hasPermission } from '../../../permissions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
config: state.authAdmin.get('saml'),
|
||||||
|
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Container = connect(mapStateToProps, { getSamlConfig, updateSamlConfig })(SamlAuth);
|
||||||
|
|
||||||
|
export default Container;
|
183
frontend/src/page/admin/auth/saml-auth.jsx
Normal file
183
frontend/src/page/admin/auth/saml-auth.jsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, Grid, Cell, Switch, Textfield } from 'react-mdl';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
enabled: false,
|
||||||
|
autoCreate: false,
|
||||||
|
unleashHostname: location.hostname,
|
||||||
|
};
|
||||||
|
|
||||||
|
function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
|
||||||
|
const [data, setData] = useState(initialState);
|
||||||
|
const [info, setInfo] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSamlConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (config.entityId) {
|
||||||
|
setData(config);
|
||||||
|
}
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
if (!hasPermission('ADMIN')) {
|
||||||
|
return <span>You need admin privileges to access this section.</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateField = e => {
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateEnabled = () => {
|
||||||
|
setData({ ...data, enabled: !data.enabled });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAutoCreate = () => {
|
||||||
|
setData({ ...data, autoCreate: !data.autoCreate });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setInfo('...saving');
|
||||||
|
try {
|
||||||
|
await updateSamlConfig(data);
|
||||||
|
setInfo('Settings stored');
|
||||||
|
setTimeout(() => setInfo(''), 2000);
|
||||||
|
} catch (e) {
|
||||||
|
setInfo(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Grid style={{ background: '#EFEFEF' }}>
|
||||||
|
<Cell col={12}>
|
||||||
|
<p>
|
||||||
|
Please read the{' '}
|
||||||
|
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication" target="_blank">
|
||||||
|
documentation
|
||||||
|
</a>{' '}
|
||||||
|
to learn how to integrate with specific SMAL 2.0 providers (Okta, Keycloak, etc). <br />
|
||||||
|
<br />
|
||||||
|
Callback URL: <code>https://[unleash.hostname.com]/auth/saml/callback</code>
|
||||||
|
</p>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Enable</strong>
|
||||||
|
<p>Enable SAML 2.0 Authentication.</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6} style={{ padding: '20px' }}>
|
||||||
|
<Switch onChange={updateEnabled} name="enabled" checked={data.enabled}>
|
||||||
|
{data.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</Switch>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Entity ID</strong>
|
||||||
|
<p>(Required) The Entity Identity provider issuer.</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6}>
|
||||||
|
<Textfield
|
||||||
|
onChange={updateField}
|
||||||
|
label="Entity ID"
|
||||||
|
name="entityId"
|
||||||
|
placeholder=""
|
||||||
|
value={data.entityId}
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Single Sign-On URL</strong>
|
||||||
|
<p>(Required) The url to redirect the user to for signing in.</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6}>
|
||||||
|
<Textfield
|
||||||
|
onChange={updateField}
|
||||||
|
label="Single Sign-On URL"
|
||||||
|
name="signOnUrl"
|
||||||
|
value={data.signOnUrl}
|
||||||
|
placeholder=""
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>X.509 Certificate</strong>
|
||||||
|
<p>(Required) The certificate used to sign the SAML 2.0 request.</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={7}>
|
||||||
|
<textarea
|
||||||
|
onChange={updateField}
|
||||||
|
label="X.509 Certificate"
|
||||||
|
name="certificate"
|
||||||
|
placeholder=""
|
||||||
|
value={data.certificate}
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '100%', fontSize: '0.7em', fontFamily: 'monospace' }}
|
||||||
|
rows={14}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Auto-create users</strong>
|
||||||
|
<p>Enable automatic creation of new users when signing in with Saml.</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6} style={{ padding: '20px' }}>
|
||||||
|
<Switch onChange={updateAutoCreate} name="enabled" checked={data.autoCreate}>
|
||||||
|
Auto-create users
|
||||||
|
</Switch>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<strong>Email domains</strong>
|
||||||
|
<p>(Optional) Comma separated list of email domains that should be allowed to sign in.</p>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={6}>
|
||||||
|
<Textfield
|
||||||
|
onChange={updateField}
|
||||||
|
label="Email domains"
|
||||||
|
name="emailDomains"
|
||||||
|
value={data.emailDomains}
|
||||||
|
placeholder="@company.com, @anotherCompany.com"
|
||||||
|
floatingLabel
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Cell col={5}>
|
||||||
|
<Button raised accent type="submit">
|
||||||
|
Save
|
||||||
|
</Button>{' '}
|
||||||
|
<small>{info}</small>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SamlAuth.propTypes = {
|
||||||
|
config: PropTypes.object,
|
||||||
|
getSamlConfig: PropTypes.func.isRequired,
|
||||||
|
updateSamlConfig: PropTypes.func.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SamlAuth;
|
31
frontend/src/page/admin/index.js
Normal file
31
frontend/src/page/admin/index.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon, Grid, Cell } from 'react-mdl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const render = () => (
|
||||||
|
<Grid style={{ textAlign: 'center' }}>
|
||||||
|
<Cell col={4}>
|
||||||
|
<Icon name="supervised_user_circle" style={{ fontSize: '5em' }} />
|
||||||
|
<br />
|
||||||
|
<Link to="/admin/users">Users</Link>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={4}>
|
||||||
|
<Icon name="apps" style={{ fontSize: '5em' }} />
|
||||||
|
<br />
|
||||||
|
<Link to="/admin/api">API Access</Link>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={4}>
|
||||||
|
<Icon name="lock" style={{ fontSize: '5em' }} />
|
||||||
|
<br />
|
||||||
|
<Link to="/admin/auth">Authentication</Link>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
match: PropTypes.object.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
135
frontend/src/page/admin/users/add-user-component.jsx
Normal file
135
frontend/src/page/admin/users/add-user-component.jsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
import { Button, Textfield, DialogTitle, DialogContent, DialogActions, RadioGroup, Radio } from 'react-mdl';
|
||||||
|
import { trim } from '../../../component/common/util';
|
||||||
|
import { modalStyles } from './util';
|
||||||
|
|
||||||
|
Modal.setAppElement('#app');
|
||||||
|
|
||||||
|
const EMPTY = { userType: 'regular' };
|
||||||
|
|
||||||
|
function AddUser({ showDialog, closeDialog, addUser, validatePassword }) {
|
||||||
|
const [data, setData] = useState(EMPTY);
|
||||||
|
const [error, setError] = useState({});
|
||||||
|
|
||||||
|
const updateField = e => {
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFieldWithTrim = e => {
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
[e.target.name]: trim(e.target.value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!data.email) {
|
||||||
|
setError({ general: 'You must specify the email adress' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addUser(data);
|
||||||
|
setData(EMPTY);
|
||||||
|
closeDialog();
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error.message || 'Could not create user';
|
||||||
|
setError({ general: msg });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPasswordBlur = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError({ password: '' });
|
||||||
|
if (data.password) {
|
||||||
|
try {
|
||||||
|
await validatePassword(data.password);
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error.message || '';
|
||||||
|
setError({ password: msg });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setData(EMPTY);
|
||||||
|
closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={showDialog} style={modalStyles} onRequestClose={onCancel}>
|
||||||
|
<form onSubmit={submit}>
|
||||||
|
<DialogTitle>Add new user</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<p style={{ color: 'red' }}>{error.general}</p>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Full name"
|
||||||
|
name="name"
|
||||||
|
value={data.name}
|
||||||
|
error={error.name}
|
||||||
|
type="name"
|
||||||
|
onChange={updateField}
|
||||||
|
/>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
value={data.email}
|
||||||
|
error={error.email}
|
||||||
|
type="email"
|
||||||
|
onChange={updateFieldWithTrim}
|
||||||
|
/>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={data.password}
|
||||||
|
error={error.password}
|
||||||
|
onChange={updateField}
|
||||||
|
onBlur={onPasswordBlur}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<RadioGroup name="userType" value={data.userType} onChange={updateField} childContainer="div">
|
||||||
|
<Radio value="regular" ripple>
|
||||||
|
Regular user
|
||||||
|
</Radio>
|
||||||
|
<Radio value="admin" ripple>
|
||||||
|
Admin user
|
||||||
|
</Radio>
|
||||||
|
<Radio value="read" ripple>
|
||||||
|
Read only
|
||||||
|
</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button type="button" raised colored type="submit">
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddUser.propTypes = {
|
||||||
|
showDialog: PropTypes.bool.isRequired,
|
||||||
|
closeDialog: PropTypes.func.isRequired,
|
||||||
|
addUser: PropTypes.func.isRequired,
|
||||||
|
validatePassword: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddUser;
|
107
frontend/src/page/admin/users/change-password-component.jsx
Normal file
107
frontend/src/page/admin/users/change-password-component.jsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
import { Button, Textfield, DialogTitle, DialogContent, DialogActions } from 'react-mdl';
|
||||||
|
import { trim } from '../../../component/common/util';
|
||||||
|
import { modalStyles } from './util';
|
||||||
|
|
||||||
|
function ChangePassword({ showDialog, closeDialog, changePassword, validatePassword, user = {} }) {
|
||||||
|
const [data, setData] = useState({});
|
||||||
|
const [error, setError] = useState({});
|
||||||
|
|
||||||
|
const updateField = e => {
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
[e.target.name]: trim(e.target.value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!data.password || data.password.length < 8) {
|
||||||
|
setError({ password: 'You must specify a password with at least 8 chars.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(data.password === data.confirm)) {
|
||||||
|
setError({ confirm: 'Passwords does not match' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await changePassword(user, data.password);
|
||||||
|
setData({});
|
||||||
|
closeDialog();
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error.message || 'Could not update password';
|
||||||
|
setError({ general: msg });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPasswordBlur = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError({ password: '' });
|
||||||
|
if (data.password) {
|
||||||
|
try {
|
||||||
|
await validatePassword(data.password);
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error.message || '';
|
||||||
|
setError({ password: msg });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setData({});
|
||||||
|
closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={showDialog} style={modalStyles} onRequestClose={onCancel}>
|
||||||
|
<form onSubmit={submit}>
|
||||||
|
<DialogTitle>Update password</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<p>User: {user.username || user.email}</p>
|
||||||
|
<p style={{ color: 'red' }}>{error.general}</p>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="New passord"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={data.password}
|
||||||
|
error={error.password}
|
||||||
|
onChange={updateField}
|
||||||
|
onBlur={onPasswordBlur}
|
||||||
|
/>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Confirm passord"
|
||||||
|
name="confirm"
|
||||||
|
type="password"
|
||||||
|
value={data.confirm}
|
||||||
|
error={error.confirm}
|
||||||
|
onChange={updateField}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button type="button" raised colored type="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangePassword.propTypes = {
|
||||||
|
showDialog: PropTypes.bool.isRequired,
|
||||||
|
closeDialog: PropTypes.func.isRequired,
|
||||||
|
changePassword: PropTypes.func.isRequired,
|
||||||
|
validatePassword: PropTypes.func.isRequired,
|
||||||
|
user: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChangePassword;
|
19
frontend/src/page/admin/users/index.js
Normal file
19
frontend/src/page/admin/users/index.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import UsersList from './users-list-container';
|
||||||
|
import AdminMenu from '../admin-menu';
|
||||||
|
|
||||||
|
const render = () => (
|
||||||
|
<div>
|
||||||
|
<AdminMenu />
|
||||||
|
<h3>Users</h3>
|
||||||
|
<UsersList />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
match: PropTypes.object.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
102
frontend/src/page/admin/users/update-user-component.jsx
Normal file
102
frontend/src/page/admin/users/update-user-component.jsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
import { Button, Textfield, DialogTitle, DialogContent, DialogActions, RadioGroup, Radio } from 'react-mdl';
|
||||||
|
import { showPermissions, modalStyles } from './util';
|
||||||
|
|
||||||
|
Modal.setAppElement('#app');
|
||||||
|
|
||||||
|
function AddUser({ user, showDialog, closeDialog, updateUser }) {
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, setData] = useState(user);
|
||||||
|
const [error, setError] = useState({});
|
||||||
|
|
||||||
|
const updateField = e => {
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateUser(data);
|
||||||
|
closeDialog();
|
||||||
|
} catch (error) {
|
||||||
|
setError({ general: 'Could not create user' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const userType = data.userType || showPermissions(user.permissions);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={showDialog} style={modalStyles} onRequestClose={onCancel}>
|
||||||
|
<form onSubmit={submit}>
|
||||||
|
<DialogTitle>Edit user</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<p>{error.general}</p>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Full name"
|
||||||
|
name="name"
|
||||||
|
value={data.name}
|
||||||
|
error={error.name}
|
||||||
|
type="name"
|
||||||
|
onChange={updateField}
|
||||||
|
/>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
contentEditable="false"
|
||||||
|
editable="false"
|
||||||
|
readOnly
|
||||||
|
value={data.email}
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<RadioGroup name="userType" value={userType} onChange={updateField} childContainer="div">
|
||||||
|
<Radio value="regular" ripple>
|
||||||
|
Regular user
|
||||||
|
</Radio>
|
||||||
|
<Radio value="admin" ripple>
|
||||||
|
Admin user
|
||||||
|
</Radio>
|
||||||
|
<Radio value="read" ripple>
|
||||||
|
Read only
|
||||||
|
</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button type="button" raised colored type="submit">
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddUser.propTypes = {
|
||||||
|
showDialog: PropTypes.bool.isRequired,
|
||||||
|
closeDialog: PropTypes.func.isRequired,
|
||||||
|
updateUser: PropTypes.func.isRequired,
|
||||||
|
user: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddUser;
|
147
frontend/src/page/admin/users/users-list-component.jsx
Normal file
147
frontend/src/page/admin/users/users-list-component.jsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
/* eslint-disable no-alert */
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon } from 'react-mdl';
|
||||||
|
import { formatFullDateTimeWithLocale } from '../../../component/common/util';
|
||||||
|
import AddUser from './add-user-component';
|
||||||
|
import ChangePassword from './change-password-component';
|
||||||
|
import UpdateUser from './update-user-component';
|
||||||
|
import { showPermissions } from './util';
|
||||||
|
|
||||||
|
function UsersList({
|
||||||
|
fetchUsers,
|
||||||
|
removeUser,
|
||||||
|
addUser,
|
||||||
|
updateUser,
|
||||||
|
changePassword,
|
||||||
|
users,
|
||||||
|
location,
|
||||||
|
hasPermission,
|
||||||
|
validatePassword,
|
||||||
|
}) {
|
||||||
|
const [showDialog, setDialog] = useState(false);
|
||||||
|
const [pwDialog, setPwDiaog] = useState({ open: false });
|
||||||
|
const [updateDialog, setUpdateDiaog] = useState({ open: false });
|
||||||
|
const openDialog = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
setDialog(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = user => e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const doIt = confirm(`Are you sure you want to delete ${user.username || user.email}?`);
|
||||||
|
if (doIt) {
|
||||||
|
removeUser(user);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPwDialog = user => e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setPwDiaog({ open: true, user });
|
||||||
|
};
|
||||||
|
|
||||||
|
const closePwDialog = () => {
|
||||||
|
setPwDiaog({ open: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const openUpdateDialog = user => e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setUpdateDiaog({ open: true, user });
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeUpdateDialog = () => {
|
||||||
|
setUpdateDiaog({ open: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<table className="mdl-data-table mdl-shadow--2dp">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric">Id</th>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric">Created</th>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric">Username</th>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric">Name</th>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric">Access</th>
|
||||||
|
<th className="mdl-data-table__cell--non-numeric">{hasPermission('ADMIN') ? 'Action' : ''}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map(item => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>{item.id}</td>
|
||||||
|
<td>{formatFullDateTimeWithLocale(item.createdAt, location.locale)}</td>
|
||||||
|
<td style={{ textAlign: 'left' }}>{item.username || item.email}</td>
|
||||||
|
<td style={{ textAlign: 'left' }}>{item.name}</td>
|
||||||
|
<td>{showPermissions(item.permissions)}</td>
|
||||||
|
{hasPermission('ADMIN') ? (
|
||||||
|
<td>
|
||||||
|
<a href="" title="Edit" onClick={openUpdateDialog(item)}>
|
||||||
|
<Icon name="edit" />
|
||||||
|
</a>
|
||||||
|
<a href="" title="Change password" onClick={openPwDialog(item)}>
|
||||||
|
<Icon name="lock" />
|
||||||
|
</a>
|
||||||
|
<a href="" title="Remove user" onClick={onDelete(item)}>
|
||||||
|
<Icon name="delete" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
) : (
|
||||||
|
<td>
|
||||||
|
<a href="" title="Change password" onClick={openPwDialog(item)}>
|
||||||
|
<Icon name="lock" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br />
|
||||||
|
<a href="" onClick={openDialog}>
|
||||||
|
Add new user
|
||||||
|
</a>
|
||||||
|
<AddUser
|
||||||
|
showDialog={showDialog}
|
||||||
|
closeDialog={closeDialog}
|
||||||
|
addUser={addUser}
|
||||||
|
validatePassword={validatePassword}
|
||||||
|
/>
|
||||||
|
<UpdateUser
|
||||||
|
showDialog={updateDialog.open}
|
||||||
|
closeDialog={closeUpdateDialog}
|
||||||
|
updateUser={updateUser}
|
||||||
|
user={updateDialog.user}
|
||||||
|
/>
|
||||||
|
<ChangePassword
|
||||||
|
showDialog={pwDialog.open}
|
||||||
|
closeDialog={closePwDialog}
|
||||||
|
changePassword={changePassword}
|
||||||
|
validatePassword={validatePassword}
|
||||||
|
user={pwDialog.user}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UsersList.propTypes = {
|
||||||
|
users: PropTypes.array.isRequired,
|
||||||
|
fetchUsers: PropTypes.func.isRequired,
|
||||||
|
removeUser: PropTypes.func.isRequired,
|
||||||
|
addUser: PropTypes.func.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
validatePassword: PropTypes.func.isRequired,
|
||||||
|
updateUser: PropTypes.func.isRequired,
|
||||||
|
changePassword: PropTypes.func.isRequired,
|
||||||
|
location: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UsersList;
|
28
frontend/src/page/admin/users/users-list-container.js
Normal file
28
frontend/src/page/admin/users/users-list-container.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import UsersList from './users-list-component';
|
||||||
|
import {
|
||||||
|
fetchUsers,
|
||||||
|
removeUser,
|
||||||
|
addUser,
|
||||||
|
changePassword,
|
||||||
|
updateUser,
|
||||||
|
validatePassword,
|
||||||
|
} from './../../../store/e-user-admin/actions';
|
||||||
|
import { hasPermission } from '../../../permissions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
users: state.userAdmin.toJS(),
|
||||||
|
location: state.settings.toJS().location || {},
|
||||||
|
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Container = connect(mapStateToProps, {
|
||||||
|
fetchUsers,
|
||||||
|
removeUser,
|
||||||
|
addUser,
|
||||||
|
changePassword,
|
||||||
|
updateUser,
|
||||||
|
validatePassword,
|
||||||
|
})(UsersList);
|
||||||
|
|
||||||
|
export default Container;
|
31
frontend/src/page/admin/users/util.js
Normal file
31
frontend/src/page/admin/users/util.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export const showPermissions = permissions => {
|
||||||
|
if (!permissions || permissions.length === 0) {
|
||||||
|
return 'read';
|
||||||
|
} else if (permissions.includes('ADMIN')) {
|
||||||
|
return 'admin';
|
||||||
|
} else {
|
||||||
|
return 'regular';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const modalStyles = {
|
||||||
|
overlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.25)',
|
||||||
|
zIndex: 5,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
width: '500px',
|
||||||
|
maxWidth: '90%',
|
||||||
|
margin: '0',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
right: 'auto',
|
||||||
|
bottom: 'auto',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
},
|
||||||
|
};
|
@ -3,9 +3,9 @@ import { RECEIVE_CONTEXT, REMOVE_CONTEXT, ADD_CONTEXT_FIELD, UPDATE_CONTEXT_FIEL
|
|||||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
||||||
|
|
||||||
const DEFAULT_CONTEXT_FIELDS = [
|
const DEFAULT_CONTEXT_FIELDS = [
|
||||||
{ name: 'environment', inital: true },
|
{ name: 'environment', initial: true },
|
||||||
{ name: 'userId', inital: true },
|
{ name: 'userId', initial: true },
|
||||||
{ name: 'appName', inital: true },
|
{ name: 'appName', initial: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
function getInitState() {
|
function getInitState() {
|
||||||
|
56
frontend/src/store/e-admin-auth/actions.js
Normal file
56
frontend/src/store/e-admin-auth/actions.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import api from './api';
|
||||||
|
import { dispatchAndThrow } from '../util';
|
||||||
|
export const RECIEVE_GOOGLE_CONFIG = 'RECIEVE_GOOGLE_CONFIG';
|
||||||
|
export const RECIEVE_GOOGLE_CONFIG_ERROR = 'RECIEVE_GOOGLE_CONFIG_ERROR';
|
||||||
|
export const UPDATE_GOOGLE_AUTH = 'UPDATE_GOOGLE_AUTH';
|
||||||
|
export const UPDATE_GOOGLE_AUTH_ERROR = 'UPDATE_GOOGLE_AUTH_ERROR';
|
||||||
|
export const RECIEVE_SAML_CONFIG = 'RECIEVE_SAML_CONFIG';
|
||||||
|
export const RECIEVE_SAML_CONFIG_ERROR = 'RECIEVE_SAML_CONFIG_ERROR';
|
||||||
|
export const UPDATE_SAML_AUTH = 'UPDATE_SAML_AUTH';
|
||||||
|
export const UPDATE_SAML_AUTH_ERROR = 'UPDATE_SAML_AUTH_ERROR';
|
||||||
|
|
||||||
|
const debug = require('debug')('unleash:e-admin-auth-actions');
|
||||||
|
|
||||||
|
export function getGoogleConfig() {
|
||||||
|
debug('Start fetching google-auth config');
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.getGoogleConfig()
|
||||||
|
.then(config =>
|
||||||
|
dispatch({
|
||||||
|
type: RECIEVE_GOOGLE_CONFIG,
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(dispatchAndThrow(dispatch, RECIEVE_GOOGLE_CONFIG_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateGoogleConfig(data) {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.updateGoogleConfig(data)
|
||||||
|
.then(config => dispatch({ type: UPDATE_GOOGLE_AUTH, config }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, UPDATE_GOOGLE_AUTH_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSamlConfig() {
|
||||||
|
debug('Start fetching Saml-auth config');
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.getSamlConfig()
|
||||||
|
.then(config =>
|
||||||
|
dispatch({
|
||||||
|
type: RECIEVE_SAML_CONFIG,
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(dispatchAndThrow(dispatch, RECIEVE_SAML_CONFIG_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSamlConfig(data) {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.updateSamlConfig(data)
|
||||||
|
.then(config => dispatch({ type: UPDATE_SAML_AUTH, config }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, UPDATE_SAML_AUTH_ERROR));
|
||||||
|
}
|
45
frontend/src/store/e-admin-auth/api.js
Normal file
45
frontend/src/store/e-admin-auth/api.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||||
|
|
||||||
|
const GOOGLE_URI = 'api/admin/auth/google/settings';
|
||||||
|
const SAML_URI = 'api/admin/auth/saml/settings';
|
||||||
|
|
||||||
|
function getGoogleConfig() {
|
||||||
|
return fetch(GOOGLE_URI, { headers, credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGoogleConfig(data) {
|
||||||
|
return fetch(GOOGLE_URI, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSamlConfig() {
|
||||||
|
return fetch(SAML_URI, { headers, credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSamlConfig(data) {
|
||||||
|
return fetch(SAML_URI, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getGoogleConfig,
|
||||||
|
updateGoogleConfig,
|
||||||
|
getSamlConfig,
|
||||||
|
updateSamlConfig,
|
||||||
|
};
|
17
frontend/src/store/e-admin-auth/index.js
Normal file
17
frontend/src/store/e-admin-auth/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Map as $Map } from 'immutable';
|
||||||
|
import { RECIEVE_GOOGLE_CONFIG, UPDATE_GOOGLE_AUTH, RECIEVE_SAML_CONFIG, UPDATE_SAML_AUTH } from './actions';
|
||||||
|
|
||||||
|
const store = (state = new $Map({ google: {}, saml: {} }), action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case UPDATE_GOOGLE_AUTH:
|
||||||
|
case RECIEVE_GOOGLE_CONFIG:
|
||||||
|
return state.set('google', action.config);
|
||||||
|
case UPDATE_SAML_AUTH:
|
||||||
|
case RECIEVE_SAML_CONFIG:
|
||||||
|
return state.set('saml', action.config);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default store;
|
40
frontend/src/store/e-api-admin/actions.js
Normal file
40
frontend/src/store/e-api-admin/actions.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import api from './api';
|
||||||
|
import { dispatchAndThrow } from '../util';
|
||||||
|
export const RECIEVE_KEYS = 'RECIEVE_KEYS';
|
||||||
|
export const ERROR_FETCH_KEYS = 'ERROR_FETCH_KEYS';
|
||||||
|
export const REMOVE_KEY = 'REMOVE_KEY';
|
||||||
|
export const REMOVE_KEY_ERROR = 'REMOVE_KEY_ERROR';
|
||||||
|
export const ADD_KEY = 'ADD_KEY';
|
||||||
|
export const ADD_KEY_ERROR = 'ADD_KEY_ERROR';
|
||||||
|
|
||||||
|
const debug = require('debug')('unleash:e-api-admin-actions');
|
||||||
|
|
||||||
|
export function fetchApiKeys() {
|
||||||
|
debug('Start fetching api-keys');
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.fetchAll()
|
||||||
|
.then(value =>
|
||||||
|
dispatch({
|
||||||
|
type: RECIEVE_KEYS,
|
||||||
|
keys: value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_KEYS));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeKey(key) {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.remove(key)
|
||||||
|
.then(() => dispatch({ type: REMOVE_KEY, key }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, REMOVE_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addKey(data) {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.create(data)
|
||||||
|
.then(newToken => dispatch({ type: ADD_KEY, token: newToken }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ADD_KEY_ERROR));
|
||||||
|
}
|
34
frontend/src/store/e-api-admin/api.js
Normal file
34
frontend/src/store/e-api-admin/api.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||||
|
|
||||||
|
const URI = 'api/admin/api-tokens';
|
||||||
|
|
||||||
|
function fetchAll() {
|
||||||
|
return fetch(URI, { headers, credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(data) {
|
||||||
|
return fetch(URI, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(key) {
|
||||||
|
return fetch(`${URI}/${key}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchAll,
|
||||||
|
create,
|
||||||
|
remove,
|
||||||
|
};
|
17
frontend/src/store/e-api-admin/index.js
Normal file
17
frontend/src/store/e-api-admin/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { List } from 'immutable';
|
||||||
|
import { RECIEVE_KEYS, ADD_KEY, REMOVE_KEY } from './actions';
|
||||||
|
|
||||||
|
const store = (state = new List(), action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case RECIEVE_KEYS:
|
||||||
|
return new List(action.keys);
|
||||||
|
case ADD_KEY:
|
||||||
|
return state.push(action.token);
|
||||||
|
case REMOVE_KEY:
|
||||||
|
return state.filter(v => v.key !== action.key);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default store;
|
64
frontend/src/store/e-user-admin/actions.js
Normal file
64
frontend/src/store/e-user-admin/actions.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import api from './api';
|
||||||
|
import { dispatchAndThrow } from '../util';
|
||||||
|
export const START_FETCH_USERS = 'START_FETCH_USERS';
|
||||||
|
export const RECIEVE_USERS = 'RECIEVE_USERS';
|
||||||
|
export const ERROR_FETCH_USERS = 'ERROR_FETCH_USERS';
|
||||||
|
export const REMOVE_USER = 'REMOVE_USER';
|
||||||
|
export const REMOVE_USER_ERROR = 'REMOVE_USER_ERROR';
|
||||||
|
export const ADD_USER = 'ADD_USER';
|
||||||
|
export const ADD_USER_ERROR = 'ADD_USER_ERROR';
|
||||||
|
export const UPDATE_USER = 'UPDATE_USER';
|
||||||
|
export const UPDATE_USER_ERROR = 'UPDATE_USER_ERROR';
|
||||||
|
export const CHANGE_PASSWORD_ERROR = 'CHANGE_PASSWORD_ERROR';
|
||||||
|
export const VALIDATE_PASSWORD_ERROR = 'VALIDATE_PASSWORD_ERROR';
|
||||||
|
|
||||||
|
const debug = require('debug')('unleash:e-user-admin-actions');
|
||||||
|
|
||||||
|
const gotUsers = value => ({
|
||||||
|
type: RECIEVE_USERS,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function fetchUsers() {
|
||||||
|
debug('Start fetching user');
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_FETCH_USERS });
|
||||||
|
|
||||||
|
return api
|
||||||
|
.fetchAll()
|
||||||
|
.then(json => dispatch(gotUsers(json)))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_USERS));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeUser(user) {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.remove(user)
|
||||||
|
.then(() => dispatch({ type: REMOVE_USER, user }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, REMOVE_USER_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addUser(user) {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.create(user)
|
||||||
|
.then(newUser => dispatch({ type: ADD_USER, user: newUser }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ADD_USER_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUser(user) {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.update(user)
|
||||||
|
.then(newUser => dispatch({ type: UPDATE_USER, user: newUser }))
|
||||||
|
.catch(dispatchAndThrow(dispatch, UPDATE_USER_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changePassword(user, newPassword) {
|
||||||
|
return dispatch => api.changePassword(user, newPassword).catch(dispatchAndThrow(dispatch, CHANGE_PASSWORD_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validatePassword(password) {
|
||||||
|
return dispatch => api.validatePassword(password).catch(dispatchAndThrow(dispatch, VALIDATE_PASSWORD_ERROR));
|
||||||
|
}
|
66
frontend/src/store/e-user-admin/api.js
Normal file
66
frontend/src/store/e-user-admin/api.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||||
|
|
||||||
|
const URI = 'api/admin/user-admin';
|
||||||
|
|
||||||
|
function fetchAll() {
|
||||||
|
return fetch(URI, { headers, credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(user) {
|
||||||
|
return fetch(URI, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(user),
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(user) {
|
||||||
|
return fetch(`${URI}/${user.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(user),
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePassword(user, newPassword) {
|
||||||
|
return fetch(`${URI}/${user.id}/change-password`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({ password: newPassword }),
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePassword(password) {
|
||||||
|
return fetch(`${URI}/validate-password`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({ password }),
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(user) {
|
||||||
|
return fetch(`${URI}/${user.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchAll,
|
||||||
|
create,
|
||||||
|
update,
|
||||||
|
changePassword,
|
||||||
|
validatePassword,
|
||||||
|
remove,
|
||||||
|
};
|
25
frontend/src/store/e-user-admin/index.js
Normal file
25
frontend/src/store/e-user-admin/index.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { List } from 'immutable';
|
||||||
|
import { RECIEVE_USERS, ADD_USER, REMOVE_USER, UPDATE_USER } from './actions';
|
||||||
|
|
||||||
|
const store = (state = new List(), action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case RECIEVE_USERS:
|
||||||
|
return new List(action.value);
|
||||||
|
case ADD_USER:
|
||||||
|
return state.push(action.user);
|
||||||
|
case UPDATE_USER:
|
||||||
|
return state.map(user => {
|
||||||
|
if (user.id === action.user.id) {
|
||||||
|
return action.user;
|
||||||
|
} else {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
case REMOVE_USER:
|
||||||
|
return state.filter(v => v.id !== action.user.id);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default store;
|
@ -1,7 +1,7 @@
|
|||||||
import { List } from 'immutable';
|
import { List } from 'immutable';
|
||||||
import { RECEIVE_FEATURE_TYPES } from './actions';
|
import { RECEIVE_FEATURE_TYPES } from './actions';
|
||||||
|
|
||||||
const DEFAULT_FEATURE_TYPES = [{ id: 'release', name: 'Release', inital: true }];
|
const DEFAULT_FEATURE_TYPES = [{ id: 'release', name: 'Release', initial: true }];
|
||||||
|
|
||||||
function getInitState() {
|
function getInitState() {
|
||||||
return new List(DEFAULT_FEATURE_TYPES);
|
return new List(DEFAULT_FEATURE_TYPES);
|
||||||
|
@ -16,6 +16,9 @@ import uiConfig from './ui-config';
|
|||||||
import context from './context';
|
import context from './context';
|
||||||
import projects from './project';
|
import projects from './project';
|
||||||
import addons from './addons';
|
import addons from './addons';
|
||||||
|
import userAdmin from './e-user-admin';
|
||||||
|
import apiAdmin from './e-api-admin';
|
||||||
|
import authAdmin from './e-admin-auth';
|
||||||
|
|
||||||
const unleashStore = combineReducers({
|
const unleashStore = combineReducers({
|
||||||
features,
|
features,
|
||||||
@ -35,6 +38,9 @@ const unleashStore = combineReducers({
|
|||||||
context,
|
context,
|
||||||
projects,
|
projects,
|
||||||
addons,
|
addons,
|
||||||
|
userAdmin,
|
||||||
|
apiAdmin,
|
||||||
|
authAdmin,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default unleashStore;
|
export default unleashStore;
|
||||||
|
@ -4,13 +4,14 @@ import { fetchFeatureTypes } from './feature-type/actions';
|
|||||||
import { fetchProjects } from './project/actions';
|
import { fetchProjects } from './project/actions';
|
||||||
import { fetchStrategies } from './strategy/actions';
|
import { fetchStrategies } from './strategy/actions';
|
||||||
import { fetchTagTypes } from './tag-type/actions';
|
import { fetchTagTypes } from './tag-type/actions';
|
||||||
|
import { C, P } from '../component/common/flags';
|
||||||
|
|
||||||
export function loadInitalData() {
|
export function loadInitialData(flags = {}) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
fetchUIConfig()(dispatch);
|
fetchUIConfig()(dispatch);
|
||||||
fetchContext()(dispatch);
|
if (flags[C]) fetchContext()(dispatch);
|
||||||
fetchFeatureTypes()(dispatch);
|
fetchFeatureTypes()(dispatch);
|
||||||
fetchProjects()(dispatch);
|
if (flags[P]) fetchProjects()(dispatch);
|
||||||
fetchStrategies()(dispatch);
|
fetchStrategies()(dispatch);
|
||||||
fetchTagTypes()(dispatch);
|
fetchTagTypes()(dispatch);
|
||||||
};
|
};
|
||||||
|
@ -15,8 +15,6 @@ const upProject = project => ({ type: UPDATE_PROJECT, project });
|
|||||||
const delProject = project => ({ type: REMOVE_PROJECT, project });
|
const delProject = project => ({ type: REMOVE_PROJECT, project });
|
||||||
|
|
||||||
export function fetchProjects() {
|
export function fetchProjects() {
|
||||||
return () => {};
|
|
||||||
/*
|
|
||||||
const receiveProjects = value => ({ type: RECEIVE_PROJECT, value });
|
const receiveProjects = value => ({ type: RECEIVE_PROJECT, value });
|
||||||
return dispatch =>
|
return dispatch =>
|
||||||
api
|
api
|
||||||
@ -25,7 +23,6 @@ export function fetchProjects() {
|
|||||||
dispatch(receiveProjects(json.projects));
|
dispatch(receiveProjects(json.projects));
|
||||||
})
|
})
|
||||||
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_PROJECT));
|
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_PROJECT));
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeProject(project) {
|
export function removeProject(project) {
|
||||||
|
@ -2,7 +2,7 @@ import { List } from 'immutable';
|
|||||||
import { RECEIVE_PROJECT, REMOVE_PROJECT, ADD_PROJECT, UPDATE_PROJECT } from './actions';
|
import { RECEIVE_PROJECT, REMOVE_PROJECT, ADD_PROJECT, UPDATE_PROJECT } from './actions';
|
||||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
||||||
|
|
||||||
const DEFAULT_PROJECTS = [{ id: 'default', name: 'Default', inital: true }];
|
const DEFAULT_PROJECTS = [{ id: 'default', name: 'Default', initial: true }];
|
||||||
|
|
||||||
function getInitState() {
|
function getInitState() {
|
||||||
return new List(DEFAULT_PROJECTS);
|
return new List(DEFAULT_PROJECTS);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
exports[`should be default state 1`] = `
|
exports[`should be default state 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"environment": "",
|
"environment": "",
|
||||||
|
"flags": Object {},
|
||||||
"headerBackground": undefined,
|
"headerBackground": undefined,
|
||||||
"links": Array [
|
"links": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -27,6 +28,7 @@ Object {
|
|||||||
exports[`should be merged state all 1`] = `
|
exports[`should be merged state all 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"environment": "dev",
|
"environment": "dev",
|
||||||
|
"flags": Object {},
|
||||||
"headerBackground": "red",
|
"headerBackground": "red",
|
||||||
"links": Array [
|
"links": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -51,6 +53,7 @@ Object {
|
|||||||
exports[`should only update headerBackground 1`] = `
|
exports[`should only update headerBackground 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"environment": "",
|
"environment": "",
|
||||||
|
"flags": Object {},
|
||||||
"headerBackground": "black",
|
"headerBackground": "black",
|
||||||
"links": Array [
|
"links": Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -15,6 +15,7 @@ const DEFAULT = new $Map({
|
|||||||
version: '3.x',
|
version: '3.x',
|
||||||
environment: '',
|
environment: '',
|
||||||
slogan: 'The enterprise ready feature toggle service.',
|
slogan: 'The enterprise ready feature toggle service.',
|
||||||
|
flags: {},
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
value: 'User documentation',
|
value: 'User documentation',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import api from './api';
|
import api from './api';
|
||||||
import { dispatchAndThrow } from '../util';
|
import { dispatchAndThrow } from '../util';
|
||||||
export const UPDATE_USER = 'UPDATE_USER';
|
export const USER_CHANGE_CURRENT = 'USER_CHANGE_CURRENT';
|
||||||
export const USER_LOGOUT = 'USER_LOGOUT';
|
export const USER_LOGOUT = 'USER_LOGOUT';
|
||||||
export const USER_LOGIN = 'USER_LOGIN';
|
export const USER_LOGIN = 'USER_LOGIN';
|
||||||
export const START_FETCH_USER = 'START_FETCH_USER';
|
export const START_FETCH_USER = 'START_FETCH_USER';
|
||||||
@ -8,7 +8,7 @@ export const ERROR_FETCH_USER = 'ERROR_FETCH_USER';
|
|||||||
const debug = require('debug')('unleash:user-actions');
|
const debug = require('debug')('unleash:user-actions');
|
||||||
|
|
||||||
const updateUser = value => ({
|
const updateUser = value => ({
|
||||||
type: UPDATE_USER,
|
type: USER_CHANGE_CURRENT,
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -28,12 +28,12 @@ export function fetchUser() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unsecureLogin(path, user) {
|
export function insecureLogin(path, user) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch({ type: START_FETCH_USER });
|
dispatch({ type: START_FETCH_USER });
|
||||||
|
|
||||||
return api
|
return api
|
||||||
.unsecureLogin(path, user)
|
.insecureLogin(path, user)
|
||||||
.then(json => dispatch(updateUser(json)))
|
.then(json => dispatch(updateUser(json)))
|
||||||
.catch(handleError);
|
.catch(handleError);
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ function fetchUser() {
|
|||||||
.then(response => response.json());
|
.then(response => response.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsecureLogin(path, user) {
|
function insecureLogin(path, user) {
|
||||||
return fetch(path, { method: 'POST', credentials: 'include', headers, body: JSON.stringify(user) })
|
return fetch(path, { method: 'POST', credentials: 'include', headers, body: JSON.stringify(user) })
|
||||||
.then(throwIfNotSuccess)
|
.then(throwIfNotSuccess)
|
||||||
.then(response => response.json());
|
.then(response => response.json());
|
||||||
@ -33,7 +33,7 @@ function passwordLogin(path, data) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
fetchUser,
|
fetchUser,
|
||||||
unsecureLogin,
|
insecureLogin,
|
||||||
logoutUser,
|
logoutUser,
|
||||||
passwordLogin,
|
passwordLogin,
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Map as $Map } from 'immutable';
|
import { Map as $Map } from 'immutable';
|
||||||
import { UPDATE_USER, USER_LOGOUT } from './actions';
|
import { USER_CHANGE_CURRENT, USER_LOGOUT } from './actions';
|
||||||
import { AUTH_REQUIRED } from '../util';
|
import { AUTH_REQUIRED } from '../util';
|
||||||
|
|
||||||
const userStore = (state = new $Map(), action) => {
|
const userStore = (state = new $Map(), action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case UPDATE_USER:
|
case USER_CHANGE_CURRENT:
|
||||||
state = state
|
state = state
|
||||||
.set('profile', action.value)
|
.set('profile', action.value)
|
||||||
.set('showDialog', false)
|
.set('showDialog', false)
|
||||||
|
Loading…
Reference in New Issue
Block a user