From 5342c86b60742064d9c9597fcb38067bf518de5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Wed, 24 Feb 2021 11:03:18 +0100 Subject: [PATCH] fix: one and only one front (#244) Co-authored-by: Christopher Kolstad Co-authored-by: Fredrik Strand Oseberg --- frontend/CHANGELOG.md | 6 +- frontend/public/favicon.ico | Bin 17910 -> 41662 bytes frontend/public/favicon_old.ico | Bin 0 -> 17910 bytes frontend/public/logo.png | Bin 5627 -> 2492 bytes frontend/public/logo_old.png | Bin 0 -> 5627 bytes .../component/addons/form-addon-container.js | 2 +- .../application/application-list-component.js | 4 +- frontend/src/component/common/flags.js | 2 + frontend/src/component/common/index.js | 17 +- .../context/edit-context-container.js | 3 + .../context/form-context-component.jsx | 20 +- .../feature/feature-type-select-component.jsx | 2 +- .../component/feature/list/list-container.jsx | 8 + .../feature/list/project-component.jsx | 11 +- .../feature/project-select-component.jsx | 11 +- .../feature/project-select-container.jsx | 12 +- .../strategy-constraint-input-container.jsx | 13 ++ .../strategy-constraint-input-field.jsx | 128 ++++++++++++ .../constraint/strategy-constraint-input.jsx | 87 ++++++++ .../strategy/strategy-configure-component.jsx | 6 + .../update-variant-component-test.jsx | 1 + .../component/feature/variant/add-variant.jsx | 2 +- .../feature/variant/e-override-config.jsx | 81 ++++++++ .../__snapshots__/footer-test.jsx.snap | 28 +++ .../__snapshots__/routes-test.jsx.snap | 45 ++++- .../component/menu/__tests__/routes-test.jsx | 4 +- frontend/src/component/menu/drawer.jsx | 12 +- frontend/src/component/menu/header.jsx | 13 +- frontend/src/component/menu/routes.js | 16 +- .../user/authentication-component.jsx | 6 +- .../user/authentication-container.jsx | 15 +- .../authentication-password-component.jsx | 4 +- .../user/authentication-simple-component.jsx | 4 +- frontend/src/page/admin/admin-menu.jsx | 13 ++ frontend/src/page/admin/api/api-howto.jsx | 25 +++ .../src/page/admin/api/api-key-create.jsx | 64 ++++++ .../page/admin/api/api-key-list-container.js | 14 ++ frontend/src/page/admin/api/api-key-list.jsx | 89 ++++++++ frontend/src/page/admin/api/index.js | 20 ++ frontend/src/page/admin/api/secret.jsx | 31 +++ .../page/admin/auth/google-auth-container.js | 13 ++ frontend/src/page/admin/auth/google-auth.jsx | 191 ++++++++++++++++++ frontend/src/page/admin/auth/index.js | 31 +++ .../page/admin/auth/saml-auth-container.js | 13 ++ frontend/src/page/admin/auth/saml-auth.jsx | 183 +++++++++++++++++ frontend/src/page/admin/index.js | 31 +++ .../page/admin/users/add-user-component.jsx | 135 +++++++++++++ .../admin/users/change-password-component.jsx | 107 ++++++++++ frontend/src/page/admin/users/index.js | 19 ++ .../admin/users/update-user-component.jsx | 102 ++++++++++ .../page/admin/users/users-list-component.jsx | 147 ++++++++++++++ .../page/admin/users/users-list-container.js | 28 +++ frontend/src/page/admin/users/util.js | 31 +++ frontend/src/store/context/index.js | 6 +- frontend/src/store/e-admin-auth/actions.js | 56 +++++ frontend/src/store/e-admin-auth/api.js | 45 +++++ frontend/src/store/e-admin-auth/index.js | 17 ++ frontend/src/store/e-api-admin/actions.js | 40 ++++ frontend/src/store/e-api-admin/api.js | 34 ++++ frontend/src/store/e-api-admin/index.js | 17 ++ frontend/src/store/e-user-admin/actions.js | 64 ++++++ frontend/src/store/e-user-admin/api.js | 66 ++++++ frontend/src/store/e-user-admin/index.js | 25 +++ frontend/src/store/feature-type/index.js | 2 +- frontend/src/store/index.js | 6 + frontend/src/store/loader.js | 7 +- frontend/src/store/project/actions.js | 3 - frontend/src/store/project/index.js | 2 +- .../ui-config-store.test.js.snap | 3 + frontend/src/store/ui-config/index.js | 1 + frontend/src/store/user/actions.js | 8 +- frontend/src/store/user/api.js | 4 +- frontend/src/store/user/index.js | 4 +- 73 files changed, 2179 insertions(+), 81 deletions(-) create mode 100644 frontend/public/favicon_old.ico create mode 100644 frontend/public/logo_old.png create mode 100644 frontend/src/component/common/flags.js create mode 100644 frontend/src/component/feature/strategy/constraint/strategy-constraint-input-container.jsx create mode 100644 frontend/src/component/feature/strategy/constraint/strategy-constraint-input-field.jsx create mode 100644 frontend/src/component/feature/strategy/constraint/strategy-constraint-input.jsx create mode 100644 frontend/src/component/feature/variant/e-override-config.jsx create mode 100644 frontend/src/page/admin/admin-menu.jsx create mode 100644 frontend/src/page/admin/api/api-howto.jsx create mode 100644 frontend/src/page/admin/api/api-key-create.jsx create mode 100644 frontend/src/page/admin/api/api-key-list-container.js create mode 100644 frontend/src/page/admin/api/api-key-list.jsx create mode 100644 frontend/src/page/admin/api/index.js create mode 100644 frontend/src/page/admin/api/secret.jsx create mode 100644 frontend/src/page/admin/auth/google-auth-container.js create mode 100644 frontend/src/page/admin/auth/google-auth.jsx create mode 100644 frontend/src/page/admin/auth/index.js create mode 100644 frontend/src/page/admin/auth/saml-auth-container.js create mode 100644 frontend/src/page/admin/auth/saml-auth.jsx create mode 100644 frontend/src/page/admin/index.js create mode 100644 frontend/src/page/admin/users/add-user-component.jsx create mode 100644 frontend/src/page/admin/users/change-password-component.jsx create mode 100644 frontend/src/page/admin/users/index.js create mode 100644 frontend/src/page/admin/users/update-user-component.jsx create mode 100644 frontend/src/page/admin/users/users-list-component.jsx create mode 100644 frontend/src/page/admin/users/users-list-container.js create mode 100644 frontend/src/page/admin/users/util.js create mode 100644 frontend/src/store/e-admin-auth/actions.js create mode 100644 frontend/src/store/e-admin-auth/api.js create mode 100644 frontend/src/store/e-admin-auth/index.js create mode 100644 frontend/src/store/e-api-admin/actions.js create mode 100644 frontend/src/store/e-api-admin/api.js create mode 100644 frontend/src/store/e-api-admin/index.js create mode 100644 frontend/src/store/e-user-admin/actions.js create mode 100644 frontend/src/store/e-user-admin/api.js create mode 100644 frontend/src/store/e-user-admin/index.js diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md index cd94c37e68..daafe85fb4 100644 --- a/frontend/CHANGELOG.md +++ b/frontend/CHANGELOG.md @@ -112,8 +112,8 @@ The latest version of this document is always available in - feat: added time-ago to toggle-list - feat: Add stale marking of feature toggles - feat: add support for toggle type (#220) -- feat: stort by stale -- fix: imporve type-chip color +- feat: sort by stale +- fix: improve type-chip color - fix: some ux cleanup for toggle types # [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: Update react-dnd to the latest version 🚀 (#213) - fix: read unleash version from ui-config (#219) -- fix: flag inital context fields +- fix: flag initial context fields # [3.3.5] diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index fed2443a05b0da964963d060996d31611152fb83..857aef80d573d92ab3726711fe281841a4780285 100644 GIT binary patch literal 41662 zcmeI5TSzoz7{^C8T|sKoM92iw&!PhrB+-TDA-~X~qY%O8MMS1XP*N9_VSZwEz#A{q zA~Xn+yb$cd@LZY)gP?{kc_<-$`#-a@>*A-o^UmyHX8k|%xIQz_d;0y~ zncbOv2MwbDKi%C1e&1?*`MF_K7>02N9Gi?E;Idt;S=Ep8B5{*H`eD;Wxv*WI=T1CHPP9eC3~P zT>oL*3-CwbxwHZ_{sR0Dc(VSUr+q(y#Y6CH+XXcEJ^Vbewma4%aQO^=JvRm z-Ts}|ysKbzPlg3~1@#|N3)^#X2do~+kS4EF2irO%o)i87x8^i!atZaxc`>ma{%xO! zoZr)R?viVYJ!;{4F8&IJZm%VaP{%LS!nGE$bBTQJa$|3yaI7xhzE)ONs@~pSH8C-v z=I7_t;^LwspPijm`}si`S-czCGN zZIMVs4GavZ;^Jc6v0edFxt7+o_fSB$EzjgPHa4pD_4Rbia7}&7%gd^|x>|SKy7yM( zp44A{)uX2jwat|9vpHFu2a8fazmkzT3cJo6#6skotvA} zVIbGkw`gecgUg~jtZsqyvm<3LP zIe#Ca9=X>baUSHU9v>!-(NBN-P>-j_ zeB`!*y1F_Yb`Pc!1N)!xs~)Ybt!{C;D!;e4r-H$tjvn%SOe(tz2|9IpCbzJ#P;G5( zxnk<3u;zD!@_dXP3n0n2&(e&Ijk!VUs%(3EyUyIy`40VSXrfcMXL8HR%GAcjhAXa4 z3NI`y=rWMCZFa2(iMsWBCSTV292^`t!Rn|?I2=|L6&1SUmvy*yEri5nT3K09H8nN5FH@D=Xs~=cV+GGgrOZrKL`}{EjimI&~*D zi9$}>Wx3PYJ?}&atISP!-J~XIo0H#xrIWSPmK^5GWB4uWBnF_3FIS=*Kl^X+%I z7`zwek*wE!Djh;tb;vR1`_Ee#L$0)WEsa{ls=5w;;J=0CY)rXg>84#dE?$iR%)qN0 zC-Qw2-%nD{%Uy7KE(02zR^{W^xB+)Fck;RH&tP-sY2Y%rtqgU449|;eMvNmJ^W$&S z^9H;So-4Zz4Sxdn_5CjN-;EyYC_e<3IqDUYXWd_f&%w9gZpP<_vcF-BQ}8F?xjY`w zd`gq=v~Gt>U*-9QH{l<^<+FeveZ(4r+y}e}uY%J^fCNZ@1W14cNPq-LfCNZ@1W14c zNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1Uy6_@nO;k`o;K|Kz4Y@qXMv? zQqlIpW{BmdR&0evbu^aM(xwHY@xQUSAR0?0QH#dmSZEBH^)JQaW;4DVkE_i%9vUHu zt=`GjlW|b?$Gc+~fz#r6_pJR1ae?HgiY3P~Ge03V|LI~L$0x^Yi&p~AQ}sBhSAxE# zo+o2jC~TRjzq%X?jp%$ij-&HwDZYP5v{B)BtjsvxL2JIoI~0iSk2e^y;w7tsW-GQT R2*i$;Y#*;|FGfK0{2$vvR1g3F literal 17910 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 dtAM|yj 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 CMNOdq diff --git a/frontend/public/logo_old.png b/frontend/public/logo_old.png new file mode 100644 index 0000000000000000000000000000000000000000..97e9ef9e8c82efdeafe2a02bba8e396ef250b64f GIT binary patch literal 5627 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 CMNOdq literal 0 HcmV?d00001 diff --git a/frontend/src/component/addons/form-addon-container.js b/frontend/src/component/addons/form-addon-container.js index 01f2e456d5..8631f5d4a6 100644 --- a/frontend/src/component/addons/form-addon-container.js +++ b/frontend/src/component/addons/form-addon-container.js @@ -3,7 +3,7 @@ import FormComponent from './form-addon-component'; import { updateAddon, createAddon, fetchAddons } from '../../store/addons/actions'; import { cloneDeep } from 'lodash'; -// Required for to fill the inital form. +// Required for to fill the initial form. const DEFAULT_DATA = { provider: '', description: '', diff --git a/frontend/src/component/application/application-list-component.js b/frontend/src/component/application/application-list-component.js index 976d5de7a4..46416be213 100644 --- a/frontend/src/component/application/application-list-component.js +++ b/frontend/src/component/application/application-list-component.js @@ -13,8 +13,8 @@ const Empty = () => ( you will require a Client SDK.

- You can read more about the available Client SDKs in the{' '} - documentation. + You can read more about how to use Unleash in your application in the{' '} + documentation. ); diff --git a/frontend/src/component/common/flags.js b/frontend/src/component/common/flags.js new file mode 100644 index 0000000000..d365a4c113 --- /dev/null +++ b/frontend/src/component/common/flags.js @@ -0,0 +1,2 @@ +export const P = 'P'; +export const C = 'C'; diff --git a/frontend/src/component/common/index.js b/frontend/src/component/common/index.js index 09b1fba230..5e207a462a 100644 --- a/frontend/src/component/common/index.js +++ b/frontend/src/component/common/index.js @@ -140,14 +140,15 @@ IconLink.propTypes = { icon: PropTypes.string, }; -export const DropdownButton = ({ label, id, className = styles.dropdownButton, title }) => ( - ); DropdownButton.propTypes = { label: PropTypes.string, + style: PropTypes.object, id: PropTypes.string, title: PropTypes.string, }; @@ -185,3 +186,15 @@ export function calc(value, total, decimal) { export function getDisplayName(WrappedComponent) { 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', + }, + }), +}; diff --git a/frontend/src/component/context/edit-context-container.js b/frontend/src/component/context/edit-context-container.js index f48a783652..f7c7d998fe 100644 --- a/frontend/src/component/context/edit-context-container.js +++ b/frontend/src/component/context/edit-context-container.js @@ -6,6 +6,9 @@ const mapStateToProps = (state, props) => { const contextFieldBase = { name: '', description: '', legalValues: [] }; const field = state.context.toJS().find(n => n.name === props.contextFieldName); const contextField = Object.assign(contextFieldBase, field); + if (!field) { + contextField.initial = true; + } return { contextField, diff --git a/frontend/src/component/context/form-context-component.jsx b/frontend/src/component/context/form-context-component.jsx index 75b519d32e..8da86e2214 100644 --- a/frontend/src/component/context/form-context-component.jsx +++ b/frontend/src/component/context/form-context-component.jsx @@ -5,6 +5,14 @@ import { Button, Chip, Textfield, Card, CardTitle, CardText, CardActions, Checkb import { FormButtons, styles as commonStyles } from '../common'; 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 { constructor(props) { super(props); @@ -17,7 +25,7 @@ class AddContextComponent extends Component { } static getDerivedStateFromProps(props, state) { - if (!state.contextField.name && props.contextField.name) { + if (state.contextField.initial && !props.contextField.initial) { return { contextField: props.contextField }; } else { return null; @@ -62,6 +70,10 @@ class AddContextComponent extends Component { evt.preventDefault(); const { contextField, currentLegalValue, errors } = this.state; + if (!currentLegalValue) { + return; + } + if (contextField.legalValues.indexOf(currentLegalValue) !== -1) { errors.currentLegalValue = 'Duplicate legal value'; this.setState({ errors }); @@ -69,7 +81,7 @@ class AddContextComponent extends Component { } const legalValues = contextField.legalValues.concat(trim(currentLegalValue)); - contextField.legalValues = legalValues; + contextField.legalValues = legalValues.sort(sortIgnoreCase); this.setState({ contextField, currentLegalValue: '', @@ -148,7 +160,9 @@ class AddContextComponent extends Component { error={errors.currentLegalValue} onChange={this.updateCurrentLegalValue} /> - +
{contextField.legalValues.map(this.renderLegalValue)}

diff --git a/frontend/src/component/feature/feature-type-select-component.jsx b/frontend/src/component/feature/feature-type-select-component.jsx index c286cd4968..ddbee02f73 100644 --- a/frontend/src/component/feature/feature-type-select-component.jsx +++ b/frontend/src/component/feature/feature-type-select-component.jsx @@ -5,7 +5,7 @@ import MySelect from '../common/select'; class FeatureTypeSelectComponent extends Component { componentDidMount() { const { fetchFeatureTypes, types } = this.props; - if (types[0].inital && fetchFeatureTypes) { + if (types[0].initial && fetchFeatureTypes) { this.props.fetchFeatureTypes(); } } diff --git a/frontend/src/component/feature/list/list-container.jsx b/frontend/src/component/feature/list/list-container.jsx index 3ada7dd8dd..836e85d12a 100644 --- a/frontend/src/component/feature/list/list-container.jsx +++ b/frontend/src/component/feature/list/list-container.jsx @@ -5,6 +5,13 @@ import { updateSettingForGroup } from '../../../store/settings/actions'; import FeatureListComponent from './list-component'; 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 => { const featureMetrics = state.featureMetrics.toJS(); const settings = state.settings.toJS().feature || {}; @@ -19,6 +26,7 @@ export const mapStateToPropsConfigurable = isFeature => state => { const regex = new RegExp(settings.filter, 'i'); features = features.filter( feature => + feature.strategies.some(s => checkConstraints(s, regex)) || regex.test(feature.name) || regex.test(feature.description) || feature.strategies.some(s => s && s.name && regex.test(s.name)) || diff --git a/frontend/src/component/feature/list/project-component.jsx b/frontend/src/component/feature/list/project-component.jsx index bd0b9493de..ddc313e74c 100644 --- a/frontend/src/component/feature/list/project-component.jsx +++ b/frontend/src/component/feature/list/project-component.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Menu, MenuItem } from 'react-mdl'; import { DropdownButton } from '../../common'; import PropTypes from 'prop-types'; @@ -13,13 +13,7 @@ function projectItem(selectedId, item) { ); } -function ProjectComponent({ fetchProjects, projects, currentProjectId, updateCurrentProject }) { - useEffect(() => { - if (projects[0].inital) { - fetchProjects(); - } - }); - +function ProjectComponent({ projects, currentProjectId, updateCurrentProject }) { function setProject(v) { const id = typeof v === 'string' ? v.trim() : ''; updateCurrentProject(id); @@ -38,6 +32,7 @@ function ProjectComponent({ fetchProjects, projects, currentProjectId, updateCur { - const projects = state.projects.toJS(); - - return { - projects, - }; -}; +const mapStateToProps = state => ({ + projects: state.projects.toJS(), + enabled: !!state.uiConfig.toJS().flags[P], +}); const ProjectContainer = connect(mapStateToProps, { fetchProjects })(ProjectSelectComponent); diff --git a/frontend/src/component/feature/strategy/constraint/strategy-constraint-input-container.jsx b/frontend/src/component/feature/strategy/constraint/strategy-constraint-input-container.jsx new file mode 100644 index 0000000000..ae3ac21c9e --- /dev/null +++ b/frontend/src/component/feature/strategy/constraint/strategy-constraint-input-container.jsx @@ -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); diff --git a/frontend/src/component/feature/strategy/constraint/strategy-constraint-input-field.jsx b/frontend/src/component/feature/strategy/constraint/strategy-constraint-input-field.jsx new file mode 100644 index 0000000000..3e10e8635d --- /dev/null +++ b/frontend/src/component/feature/strategy/constraint/strategy-constraint-input-field.jsx @@ -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 ( + + + updateConstraint(evt.target.value, 'contextName')} + style={{ width: 'auto' }} + /> + + + updateConstraint(evt.target.value, 'operator')} + style={{ width: 'auto' }} + /> + + + {options ? ( + + + ) : ( + + )} + + + + + + ); + }); +} + +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); diff --git a/frontend/src/component/menu/__tests__/__snapshots__/footer-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/footer-test.jsx.snap index a7615c238d..89f0ad6f8f 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/footer-test.jsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/footer-test.jsx.snap @@ -43,6 +43,20 @@ exports[`should render DrawerMenu 1`] = ` > Applications + + Context Fields + + + Projects + Applications + + Context Fields + + + Projects + { - expect(routes.length).toEqual(28); + expect(routes.length).toEqual(32); expect(routes).toMatchSnapshot(); }); test('returns all baseRoutes', () => { - expect(baseRoutes.length).toEqual(8); + expect(baseRoutes.length).toEqual(10); expect(baseRoutes).toMatchSnapshot(); }); diff --git a/frontend/src/component/menu/drawer.jsx b/frontend/src/component/menu/drawer.jsx index 1732215378..34f6e782d4 100644 --- a/frontend/src/component/menu/drawer.jsx +++ b/frontend/src/component/menu/drawer.jsx @@ -6,6 +6,13 @@ import styles from '../styles.module.scss'; import { baseRoutes as routes } from './routes'; +const filterByFlags = flags => r => { + if (r.flag && !flags[r.flag]) { + return false; + } + return true; +}; + function getIcon(name) { if (name === 'c_github') { return ; @@ -41,7 +48,7 @@ function renderLink(link) { } } -export const DrawerMenu = ({ links = [], title = 'Unleash' }) => ( +export const DrawerMenu = ({ links = [], title = 'Unleash', flags = {} }) => ( @@ -49,7 +56,7 @@ export const DrawerMenu = ({ links = [], title = 'Unleash' }) => (
- {routes.map(item => ( + {routes.filter(filterByFlags(flags)).map(item => ( ( DrawerMenu.propTypes = { links: PropTypes.array, title: PropTypes.string, + flags: PropTypes.object, }; diff --git a/frontend/src/component/menu/header.jsx b/frontend/src/component/menu/header.jsx index 366bb498f6..063e762441 100644 --- a/frontend/src/component/menu/header.jsx +++ b/frontend/src/component/menu/header.jsx @@ -6,17 +6,18 @@ import { Route } from 'react-router-dom'; import { DrawerMenu } from './drawer'; import Breadcrum from './breadcrumb'; import ShowUserContainer from '../user/show-user-container'; -import { loadInitalData } from './../../store/loader'; +import { loadInitialData } from './../../store/loader'; class HeaderComponent extends PureComponent { static propTypes = { uiConfig: PropTypes.object.isRequired, - loadInitalData: PropTypes.func.isRequired, + init: PropTypes.func.isRequired, location: PropTypes.object.isRequired, }; componentDidMount() { - this.props.loadInitalData(); + const { init, uiConfig } = this.props; + init(uiConfig.flags); } // eslint-disable-next-line camelcase @@ -35,7 +36,7 @@ class HeaderComponent extends PureComponent { } render() { - const { headerBackground, links, name } = this.props.uiConfig; + const { headerBackground, links, name, flags } = this.props.uiConfig; const style = headerBackground ? { background: headerBackground } : {}; return ( @@ -44,10 +45,10 @@ class HeaderComponent extends PureComponent { - +
); } } -export default connect(state => ({ uiConfig: state.uiConfig.toJS() }), { loadInitalData })(HeaderComponent); +export default connect(state => ({ uiConfig: state.uiConfig.toJS() }), { init: loadInitialData })(HeaderComponent); diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js index e494ecaa0e..c88da7f933 100644 --- a/frontend/src/component/menu/routes.js +++ b/frontend/src/component/menu/routes.js @@ -26,6 +26,12 @@ import CreateTag from '../../page/tags/create'; import Addons from '../../page/addons'; import AddonsCreate from '../../page/addons/create'; 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 = [ // Features { path: '/features/create', parent: '/features', title: 'Create', component: CreateFeatureToggle }, @@ -58,12 +64,18 @@ export const routes = [ // Context { path: '/context/create', parent: '/context', title: 'Create', component: CreateContextField }, { 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 { path: '/projects/create', parent: '/projects', title: 'Create', component: CreateProject }, { 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/edit/:name', parent: '/tag-types', title: ':name', component: EditTagType }, diff --git a/frontend/src/component/user/authentication-component.jsx b/frontend/src/component/user/authentication-component.jsx index 470f9b8844..63f8365fb5 100644 --- a/frontend/src/component/user/authentication-component.jsx +++ b/frontend/src/component/user/authentication-component.jsx @@ -37,7 +37,7 @@ class AuthComponent extends React.Component { user: PropTypes.object.isRequired, unsecureLogin: PropTypes.func.isRequired, passwordLogin: PropTypes.func.isRequired, - loadInitalData: PropTypes.func.isRequired, + loadInitialData: PropTypes.func.isRequired, history: PropTypes.object.isRequired, }; @@ -51,7 +51,7 @@ class AuthComponent extends React.Component { ); @@ -60,7 +60,7 @@ class AuthComponent extends React.Component { ); diff --git a/frontend/src/component/user/authentication-container.jsx b/frontend/src/component/user/authentication-container.jsx index e62202f35f..b1fbbf5349 100644 --- a/frontend/src/component/user/authentication-container.jsx +++ b/frontend/src/component/user/authentication-container.jsx @@ -1,16 +1,17 @@ import { connect } from 'react-redux'; import AuthenticationComponent from './authentication-component'; -import { unsecureLogin, passwordLogin } from '../../store/user/actions'; -import { loadInitalData } from './../../store/loader'; +import { insecureLogin, passwordLogin } from '../../store/user/actions'; +import { loadInitialData } from './../../store/loader'; -const mapDispatchToProps = { - unsecureLogin, - passwordLogin, - loadInitalData, -}; +const mapDispatchToProps = (dispatch, props) => ({ + insecureLogin: (path, user) => insecureLogin(path, user)(dispatch), + passwordLogin: (path, user) => passwordLogin(path, user)(dispatch), + loadInitialData: () => loadInitialData(props.flags)(dispatch), +}); const mapStateToProps = state => ({ user: state.user.toJS(), + flags: state.uiConfig.toJS().flags, }); export default connect(mapStateToProps, mapDispatchToProps)(AuthenticationComponent); diff --git a/frontend/src/component/user/authentication-password-component.jsx b/frontend/src/component/user/authentication-password-component.jsx index a81004829b..797b9c37a5 100644 --- a/frontend/src/component/user/authentication-password-component.jsx +++ b/frontend/src/component/user/authentication-password-component.jsx @@ -6,7 +6,7 @@ class EnterpriseAuthenticationComponent extends React.Component { static propTypes = { authDetails: PropTypes.object.isRequired, passwordLogin: PropTypes.func.isRequired, - loadInitalData: PropTypes.func.isRequired, + loadInitialData: PropTypes.func.isRequired, history: PropTypes.object.isRequired, }; @@ -37,7 +37,7 @@ class EnterpriseAuthenticationComponent extends React.Component { try { await this.props.passwordLogin(path, user); - await this.props.loadInitalData(); + await this.props.loadInitialData(); this.props.history.push(`/`); } catch (error) { if (error.statusCode === 404) { diff --git a/frontend/src/component/user/authentication-simple-component.jsx b/frontend/src/component/user/authentication-simple-component.jsx index 76d69db45d..34b6cce1be 100644 --- a/frontend/src/component/user/authentication-simple-component.jsx +++ b/frontend/src/component/user/authentication-simple-component.jsx @@ -6,7 +6,7 @@ class SimpleAuthenticationComponent extends React.Component { static propTypes = { authDetails: PropTypes.object.isRequired, unsecureLogin: PropTypes.func.isRequired, - loadInitalData: PropTypes.func.isRequired, + loadInitialData: PropTypes.func.isRequired, history: PropTypes.object.isRequired, }; @@ -18,7 +18,7 @@ class SimpleAuthenticationComponent extends React.Component { this.props .unsecureLogin(path, user) - .then(this.props.loadInitalData) + .then(this.props.loadInitialData) .then(() => this.props.history.push(`/`)); }; diff --git a/frontend/src/page/admin/admin-menu.jsx b/frontend/src/page/admin/admin-menu.jsx new file mode 100644 index 0000000000..1aa660546a --- /dev/null +++ b/frontend/src/page/admin/admin-menu.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +function AdminMenu() { + return ( +
+ Users | API Access |{' '} + Authentication +
+ ); +} + +export default AdminMenu; diff --git a/frontend/src/page/admin/api/api-howto.jsx b/frontend/src/page/admin/api/api-howto.jsx new file mode 100644 index 0000000000..3a4b6d9938 --- /dev/null +++ b/frontend/src/page/admin/api/api-howto.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +function ApiHowTo() { + return ( +
+

+ Read the{' '} + + Getting started guide + {' '} + to learn how to connect to the Unleash API form your application or programmatically.

+ Please note it can take up to 1 minute before a new API key is activated. +

+
+ ); +} + +export default ApiHowTo; diff --git a/frontend/src/page/admin/api/api-key-create.jsx b/frontend/src/page/admin/api/api-key-create.jsx new file mode 100644 index 0000000000..fc542f5897 --- /dev/null +++ b/frontend/src/page/admin/api/api-key-create.jsx @@ -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 ( +
+ {show ? ( +
+ setUsername(e.target.value)} + label="Username" + floatingLabel + style={{ width: '200px' }} + error={error} + /> + + + + + + ) : ( + + Add new access key + + )} +
+ ); +} + +CreateApiKey.propTypes = { + addKey: PropTypes.func.isRequired, +}; + +export default CreateApiKey; diff --git a/frontend/src/page/admin/api/api-key-list-container.js b/frontend/src/page/admin/api/api-key-list-container.js new file mode 100644 index 0000000000..6988d6d963 --- /dev/null +++ b/frontend/src/page/admin/api/api-key-list-container.js @@ -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); diff --git a/frontend/src/page/admin/api/api-key-list.jsx b/frontend/src/page/admin/api/api-key-list.jsx new file mode 100644 index 0000000000..582a4acdca --- /dev/null +++ b/frontend/src/page/admin/api/api-key-list.jsx @@ -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 ( +
+ + + + + + + + + + + + + {keys.map(item => ( + + + + + + {hasPermission('ADMIN') ? ( + + ) : ( + + ))} + +
+ Created + + Username + + Acess Type + + Secret + + Action +
+ {formatFullDateTimeWithLocale(item.created, location.locale)} + {item.username}{item.priviliges[0]} + + + { + e.preventDefault(); + deleteKey(item.key); + }} + > + + + + )} +
+ {hasPermission('ADMIN') ? : null} +
+ ); +} + +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; diff --git a/frontend/src/page/admin/api/index.js b/frontend/src/page/admin/api/index.js new file mode 100644 index 0000000000..8917e9bdb3 --- /dev/null +++ b/frontend/src/page/admin/api/index.js @@ -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 = () => ( +
+ +

API Access

+ +
+); + +render.propTypes = { + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, +}; + +export default render; diff --git a/frontend/src/page/admin/api/secret.jsx b/frontend/src/page/admin/api/secret.jsx new file mode 100644 index 0000000000..9af6876128 --- /dev/null +++ b/frontend/src/page/admin/api/secret.jsx @@ -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 ( +
+ {show ? ( + + ) : ( + *************************** + )} + + + + +
+ ); +} + +Secret.propTypes = { + value: PropTypes.string, +}; + +export default Secret; diff --git a/frontend/src/page/admin/auth/google-auth-container.js b/frontend/src/page/admin/auth/google-auth-container.js new file mode 100644 index 0000000000..6557d9c059 --- /dev/null +++ b/frontend/src/page/admin/auth/google-auth-container.js @@ -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; diff --git a/frontend/src/page/admin/auth/google-auth.jsx b/frontend/src/page/admin/auth/google-auth.jsx new file mode 100644 index 0000000000..e6194d0002 --- /dev/null +++ b/frontend/src/page/admin/auth/google-auth.jsx @@ -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 You need admin privileges to access this section.; + } + + 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 ( +
+ + +

+ Please read the{' '} + + documentation + {' '} + to learn how to integrate with Google OAuth 2.0.
+
+ Callback URL: https://[unleash.hostname.com]/auth/google/callback +

+
+
+
+ + + Enable +

+ Enable Google users to login. Value is ignored if Client ID and Client Secret are not + defined. +

+
+ + + {data.enabled ? 'Enabled' : 'Disabled'} + + +
+ + + Client ID +

(Required) The Client ID provided by Google when registering the application.

+
+ + + +
+ + + Client Secret +

(Required) Client Secret provided by Google when registering the application.

+
+ + + +
+ + + Unleash hostname +

+ (Required) The hostname you are running Unleash on that Google should send the user back to. + The final callback URL will be{' '} + + https://[unleash.hostname.com]/auth/google/callback + +

+
+ + + +
+ + + Auto-create users +

Enable automatic creation of new users when signing in with Google.

+
+ + + Auto-create users + + +
+ + + Email domains +

(Optional) Comma separated list of email domains that should be allowed to sign in.

+
+ + + +
+ + + {' '} + {info} + + +
+
+ ); +} + +GoogleAuth.propTypes = { + config: PropTypes.object, + getGoogleConfig: PropTypes.func.isRequired, + updateGoogleConfig: PropTypes.func.isRequired, + hasPermission: PropTypes.func.isRequired, +}; + +export default GoogleAuth; diff --git a/frontend/src/page/admin/auth/index.js b/frontend/src/page/admin/auth/index.js new file mode 100644 index 0000000000..5a47ebd760 --- /dev/null +++ b/frontend/src/page/admin/auth/index.js @@ -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 ( +
+ +

Authentication

+
+ + SAML 2.0 + Google + +
{activeTab === 0 ? : }
+
+
+ ); +} + +AdminAuthPage.propTypes = { + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, +}; + +export default AdminAuthPage; diff --git a/frontend/src/page/admin/auth/saml-auth-container.js b/frontend/src/page/admin/auth/saml-auth-container.js new file mode 100644 index 0000000000..4340f8e798 --- /dev/null +++ b/frontend/src/page/admin/auth/saml-auth-container.js @@ -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; diff --git a/frontend/src/page/admin/auth/saml-auth.jsx b/frontend/src/page/admin/auth/saml-auth.jsx new file mode 100644 index 0000000000..1690af3984 --- /dev/null +++ b/frontend/src/page/admin/auth/saml-auth.jsx @@ -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 You need admin privileges to access this section.; + } + + 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 ( +
+ + +

+ Please read the{' '} + + documentation + {' '} + to learn how to integrate with specific SMAL 2.0 providers (Okta, Keycloak, etc).
+
+ Callback URL: https://[unleash.hostname.com]/auth/saml/callback +

+
+
+
+ + + Enable +

Enable SAML 2.0 Authentication.

+
+ + + {data.enabled ? 'Enabled' : 'Disabled'} + + +
+ + + Entity ID +

(Required) The Entity Identity provider issuer.

+
+ + + +
+ + + Single Sign-On URL +

(Required) The url to redirect the user to for signing in.

+
+ + + +
+ + + X.509 Certificate +

(Required) The certificate used to sign the SAML 2.0 request.

+
+ +