From 86631b53c9b1066b100702008d78ab044959e51a Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Mon, 12 Apr 2021 15:04:03 +0200 Subject: [PATCH] Fix/material UI cleanup (#264) * fix: strategy dialogue * fix: fontweight dropdown * fix: eventlog padding * refactor: history * refactor: use material ui styling conventions for history * refactor: add empty state for features * refactor: variant dialog * refactor: delete unused variant config * fix: variant typography * fix: remove unused styles file * fix: footer * feat: protected routes * fix: rename app * fix: remove console log * fix: convert app to typescript * fix: add standalone login screen * fix: cleanup * fix: add theme colors for login * fix: update tests * fix: swap route with ProtectedRoute * fix: remove unused redirect * fix: use redirect to correctly setup breadcrumbs * refactor: isUnauthorized * fix: reset loading count on logout * fix: create a more comprehensive auth check * feat: add unleash logo --- frontend/.prettierrc | 7 + frontend/public/switches.svg | 11 + frontend/src/common.styles.js | 6 + frontend/src/component/App.tsx | 82 +++++ .../ReportToggleList/ReportToggleList.jsx | 28 +- frontend/src/component/app.jsx | 44 --- .../DropdownMenu.jsx} | 15 +- .../common/PageContent/PageContent.jsx | 8 +- .../common/ProjectSelect/ProjectSelect.jsx | 24 +- .../common/ProtectedRoute/ProtectedRoute.jsx | 23 ++ frontend/src/component/common/index.js | 42 ++- frontend/src/component/common/select.jsx | 15 +- .../FeatureToggleList/FeatureToggleList.jsx | 49 ++- .../FeatureToggleListActions.jsx | 17 +- .../list-component-test.jsx.snap | 36 +- .../feature/FeatureToggleList/styles.js | 12 +- .../feature/FeatureView/FeatureView.jsx | 93 ++++- .../strategy/AddStrategy/AddStrategy.jsx | 77 +++- .../AddStrategy/AddStrategy.styles.js | 1 - .../feature/strategy/strategies-add.jsx | 12 +- .../feature/variant/AddVariant/AddVariant.jsx | 299 +++++++++++++++ .../OverrideConfig/OverrideConfig.jsx} | 43 ++- .../OverrideConfig/OverrideConfig.styles.js | 7 + .../update-variant-component-test.jsx.snap | 10 +- .../update-variant-component-test.jsx | 86 +++-- .../component/feature/variant/add-variant.jsx | 262 -------------- .../feature/variant/override-config.jsx | 54 --- .../variant/update-variant-component.jsx | 46 ++- .../view-component-test.jsx.snap | 14 +- .../feature/view/status-update-component.jsx | 4 +- .../history/EventHistory/EventHistory.tsx | 22 ++ .../component/history/EventHistory/index.js | 16 + .../history/EventLog/EventCard/EventCard.jsx | 38 ++ .../EventLog/EventCard/EventCard.styles.js | 7 + .../EventCard/EventDiff/EventDiff.jsx | 102 ++++++ .../EventCard/EventDiff/EventDiff.styles.js | 13 + .../history/EventLog/EventJson/EventJson.jsx | 28 ++ .../EventLog/EventJson/EventJson.styles.js | 10 + .../component/history/EventLog/EventLog.jsx | 93 +++++ .../history/EventLog/EventLog.styles.js | 40 +++ .../index.jsx} | 10 +- .../FeatureEventHistory.jsx | 29 ++ .../index.jsx} | 10 +- .../component/history/history-component.jsx | 34 -- .../component/history/history-container.js | 14 - .../component/history/history-item-diff.jsx | 98 ----- .../component/history/history-item-json.jsx | 29 -- .../history/history-list-component.jsx | 110 ------ .../history/history-list-toggle-component.jsx | 25 -- .../src/component/history/history.module.scss | 64 ---- .../layout/LayoutPicker/LayoutPicker.jsx | 16 + .../{main.jsx => MainLayout/MainLayout.jsx} | 32 +- frontend/src/component/menu/Footer/Footer.jsx | 74 ++-- .../component/menu/Footer/Footer.module.scss | 11 +- .../component/menu/Footer/Footer.styles.js | 21 ++ .../__snapshots__/footer-test.jsx.snap | 340 +++++++++++++++++- .../__snapshots__/routes-test.jsx.snap | 104 ++++++ .../component/menu/__tests__/footer-test.jsx | 36 +- .../component/menu/__tests__/routes-test.jsx | 2 +- frontend/src/component/menu/routes.js | 82 ++++- frontend/src/component/user/Login/Login.jsx | 61 ++++ .../src/component/user/Login/Login.styles.js | 48 +++ frontend/src/component/user/Login/index.js | 14 + .../user/PasswordAuth/PasswordAuth.jsx | 37 +- .../component/user/SimpleAuth/SimpleAuth.jsx | 25 +- .../user/SimpleAuth/SimpleAuth.module.scss | 8 +- .../user/authentication-component.jsx | 31 +- .../src/component/user/logout-component.jsx | 15 +- .../component/user/show-user-component.jsx | 46 ++- frontend/src/icons/switches.svg | 11 + frontend/src/icons/unleash-logo-inverted.svg | 4 + frontend/src/index.tsx | 14 +- frontend/src/interfaces/route.ts | 16 + frontend/src/interfaces/user.ts | 24 ++ frontend/src/page/history/index.js | 2 +- frontend/src/page/history/toggle.js | 6 +- frontend/src/store/api-calls/index.js | 9 +- frontend/src/store/feature-toggle/actions.js | 33 +- frontend/src/store/user/actions.js | 35 +- frontend/src/themes/main-theme.js | 30 +- 80 files changed, 2319 insertions(+), 1087 deletions(-) create mode 100644 frontend/.prettierrc create mode 100644 frontend/public/switches.svg create mode 100644 frontend/src/component/App.tsx delete mode 100644 frontend/src/component/app.jsx rename frontend/src/component/common/{dropdown-menu.jsx => DropdownMenu/DropdownMenu.jsx} (84%) create mode 100644 frontend/src/component/common/ProtectedRoute/ProtectedRoute.jsx create mode 100644 frontend/src/component/feature/variant/AddVariant/AddVariant.jsx rename frontend/src/component/feature/variant/{e-override-config.jsx => AddVariant/OverrideConfig/OverrideConfig.jsx} (66%) create mode 100644 frontend/src/component/feature/variant/AddVariant/OverrideConfig/OverrideConfig.styles.js delete mode 100644 frontend/src/component/feature/variant/add-variant.jsx delete mode 100644 frontend/src/component/feature/variant/override-config.jsx create mode 100644 frontend/src/component/history/EventHistory/EventHistory.tsx create mode 100644 frontend/src/component/history/EventHistory/index.js create mode 100644 frontend/src/component/history/EventLog/EventCard/EventCard.jsx create mode 100644 frontend/src/component/history/EventLog/EventCard/EventCard.styles.js create mode 100644 frontend/src/component/history/EventLog/EventCard/EventDiff/EventDiff.jsx create mode 100644 frontend/src/component/history/EventLog/EventCard/EventDiff/EventDiff.styles.js create mode 100644 frontend/src/component/history/EventLog/EventJson/EventJson.jsx create mode 100644 frontend/src/component/history/EventLog/EventJson/EventJson.styles.js create mode 100644 frontend/src/component/history/EventLog/EventLog.jsx create mode 100644 frontend/src/component/history/EventLog/EventLog.styles.js rename frontend/src/component/history/{history-list-container.jsx => EventLog/index.jsx} (53%) create mode 100644 frontend/src/component/history/FeatureEventHistory/FeatureEventHistory.jsx rename frontend/src/component/history/{history-list-toggle-container.jsx => FeatureEventHistory/index.jsx} (60%) delete mode 100644 frontend/src/component/history/history-component.jsx delete mode 100644 frontend/src/component/history/history-container.js delete mode 100644 frontend/src/component/history/history-item-diff.jsx delete mode 100644 frontend/src/component/history/history-item-json.jsx delete mode 100644 frontend/src/component/history/history-list-component.jsx delete mode 100644 frontend/src/component/history/history-list-toggle-component.jsx delete mode 100644 frontend/src/component/history/history.module.scss create mode 100644 frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx rename frontend/src/component/layout/{main.jsx => MainLayout/MainLayout.jsx} (56%) create mode 100644 frontend/src/component/menu/Footer/Footer.styles.js create mode 100644 frontend/src/component/user/Login/Login.jsx create mode 100644 frontend/src/component/user/Login/Login.styles.js create mode 100644 frontend/src/component/user/Login/index.js create mode 100644 frontend/src/icons/switches.svg create mode 100644 frontend/src/icons/unleash-logo-inverted.svg create mode 100644 frontend/src/interfaces/route.ts create mode 100644 frontend/src/interfaces/user.ts diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000000..42de18cd33 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "avoid", + "printWidth": 80 +} diff --git a/frontend/public/switches.svg b/frontend/public/switches.svg new file mode 100644 index 0000000000..aad9bd6f54 --- /dev/null +++ b/frontend/public/switches.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/common.styles.js b/frontend/src/common.styles.js index 8d74015308..ce0e58be24 100644 --- a/frontend/src/common.styles.js +++ b/frontend/src/common.styles.js @@ -32,4 +32,10 @@ export const useCommonStyles = makeStyles(theme => ({ textCenter: { textAlign: 'center', }, + fullWidth: { + width: '100%', + }, + fullHeight: { + height: '100%', + }, })); diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx new file mode 100644 index 0000000000..5ebbc79be9 --- /dev/null +++ b/frontend/src/component/App.tsx @@ -0,0 +1,82 @@ +import { connect } from 'react-redux'; +import { Route, Switch, Redirect } from 'react-router-dom'; +import { RouteComponentProps } from 'react-router'; + +import ProtectedRoute from './common/ProtectedRoute/ProtectedRoute'; +import LayoutPicker from './layout/LayoutPicker/LayoutPicker'; + +import { routes } from './menu/routes'; + +import styles from './styles.module.scss'; + +import IUser from '../interfaces/user'; +interface IAppProps extends RouteComponentProps { + user: IUser; +} + +const App = ({ location, user }: IAppProps) => { + const renderMainLayoutRoutes = () => { + return routes.filter(route => route.layout === 'main').map(renderRoute); + }; + + const renderStandaloneRoutes = () => { + return routes + .filter(route => route.layout === 'standalone') + .map(renderRoute); + }; + + const isUnauthorized = () => { + // authDetails only exists if the user is not logged in. + return ( + user?.authDetails !== undefined || Object.keys(user).length === 0 + ); + }; + + // Change this to IRoute once snags with HashRouter and TS is worked out + const renderRoute = (route: any) => { + if (route.type === 'protected') { + const unauthorized = isUnauthorized(); + + return ( + + ); + } + return ( + } + /> + ); + }; + + return ( +
+ + + + {renderMainLayoutRoutes()} + {renderStandaloneRoutes()} + + +
+ ); +}; + +// Set state to any for now, to avoid typing up entire state object while converting to tsx. +const mapStateToProps = (state: any) => ({ + user: state.user.toJS(), +}); + +export default connect(mapStateToProps)(App); diff --git a/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx index da6f1d6239..0d6346bb6a 100644 --- a/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx +++ b/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx @@ -5,9 +5,13 @@ import PropTypes from 'prop-types'; import ReportToggleListItem from './ReportToggleListItem/ReportToggleListItem'; import ReportToggleListHeader from './ReportToggleListHeader/ReportToggleListHeader'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; -import DropdownMenu from '../../common/dropdown-menu'; +import DropdownMenu from '../../common/DropdownMenu/DropdownMenu'; -import { getObjectProperties, getCheckedState, applyCheckedToFeatures } from '../utils'; +import { + getObjectProperties, + getCheckedState, + applyCheckedToFeatures, +} from '../utils'; import useSort from '../useSort'; @@ -23,7 +27,14 @@ const ReportToggleList = ({ features, selectedProject }) => { useEffect(() => { const formattedFeatures = features.map(feature => ({ - ...getObjectProperties(feature, 'name', 'lastSeenAt', 'createdAt', 'stale', 'type'), + ...getObjectProperties( + feature, + 'name', + 'lastSeenAt', + 'createdAt', + 'stale', + 'type' + ), checked: getCheckedState(feature.name, features), setFeatures, })); @@ -42,7 +53,11 @@ const ReportToggleList = ({ features, selectedProject }) => { const renderListRows = () => sort(localFeatures).map(feature => ( - + )); const renderBulkActionsMenu = () => ( @@ -62,7 +77,10 @@ const ReportToggleList = ({ features, selectedProject }) => {

Overview

- +
diff --git a/frontend/src/component/app.jsx b/frontend/src/component/app.jsx deleted file mode 100644 index 76c775b5a8..0000000000 --- a/frontend/src/component/app.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { Route, Redirect, Switch } from 'react-router-dom'; - -import Features from '../page/features'; -import AuthenticationContainer from './user/authentication-container'; -import MainLayout from './layout/main'; - -import { routes } from './menu/routes'; - -import styles from './styles.module.scss'; -class App extends PureComponent { - static propTypes = { - location: PropTypes.object.isRequired, - match: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, - user: PropTypes.object, - }; - - render() { - if (this.props.user.authDetails) { - return ; - } - return ( -
- - - } /> - {routes.map(route => ( - - ))} - - -
- ); - } -} - -const mapStateToProps = state => ({ - user: state.user.toJS(), -}); - -export default connect(mapStateToProps)(App); diff --git a/frontend/src/component/common/dropdown-menu.jsx b/frontend/src/component/common/DropdownMenu/DropdownMenu.jsx similarity index 84% rename from frontend/src/component/common/dropdown-menu.jsx rename to frontend/src/component/common/DropdownMenu/DropdownMenu.jsx index 9916d4cdd7..09e540115f 100644 --- a/frontend/src/component/common/dropdown-menu.jsx +++ b/frontend/src/component/common/DropdownMenu/DropdownMenu.jsx @@ -1,11 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Menu } from '@material-ui/core'; -import { DropdownButton } from '.'; +import { DropdownButton } from '..'; -import styles from './common.module.scss'; +import styles from '../common.module.scss'; -const DropdownMenu = ({ renderOptions, id, title, callback, icon = 'arrow_drop_down', label, startIcon, ...rest }) => { +const DropdownMenu = ({ + renderOptions, + id, + title, + callback, + icon = 'arrow_drop_down', + label, + startIcon, + ...rest +}) => { const [anchor, setAnchor] = React.useState(null); const handleOpen = e => setAnchor(e.currentTarget); diff --git a/frontend/src/component/common/PageContent/PageContent.jsx b/frontend/src/component/common/PageContent/PageContent.jsx index ba71c3c0cf..06e1c0ab70 100644 --- a/frontend/src/component/common/PageContent/PageContent.jsx +++ b/frontend/src/component/common/PageContent/PageContent.jsx @@ -6,7 +6,13 @@ import HeaderTitle from '../HeaderTitle'; import { Paper } from '@material-ui/core'; import { useStyles } from './styles'; -const PageContent = ({ children, headerContent, disablePadding, disableBorder, ...rest }) => { +const PageContent = ({ + children, + headerContent, + disablePadding, + disableBorder, + ...rest +}) => { const styles = useStyles(); const headerClasses = classnames(styles.headerContainer, { diff --git a/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx b/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx index 58175aeda2..0d14e83630 100644 --- a/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx +++ b/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx @@ -1,11 +1,15 @@ import React from 'react'; import { MenuItem } from '@material-ui/core'; import PropTypes from 'prop-types'; -import DropdownMenu from '../dropdown-menu'; +import DropdownMenu from '../DropdownMenu/DropdownMenu'; const ALL_PROJECTS = { id: '*', name: '> All projects' }; -const ProjectSelect = ({ projects, currentProjectId, updateCurrentProject }) => { +const ProjectSelect = ({ + projects, + currentProjectId, + updateCurrentProject, +}) => { const setProject = v => { const id = typeof v === 'string' ? v.trim() : ''; updateCurrentProject(id); @@ -27,19 +31,29 @@ const ProjectSelect = ({ projects, currentProjectId, updateCurrentProject }) => }; const renderProjectItem = (selectedId, item) => ( - + {item.name} ); const renderProjectOptions = () => { const start = [ - + {ALL_PROJECTS.name} , ]; - return [...start, ...projects.map(p => renderProjectItem(currentProjectId, p))]; + return [ + ...start, + ...projects.map(p => renderProjectItem(currentProjectId, p)), + ]; }; return ( diff --git a/frontend/src/component/common/ProtectedRoute/ProtectedRoute.jsx b/frontend/src/component/common/ProtectedRoute/ProtectedRoute.jsx new file mode 100644 index 0000000000..94371efce9 --- /dev/null +++ b/frontend/src/component/common/ProtectedRoute/ProtectedRoute.jsx @@ -0,0 +1,23 @@ +import { Route, Redirect } from 'react-router-dom'; + +const ProtectedRoute = ({ + component: Component, + unauthorized, + renderProps = {}, + ...rest +}) => { + return ( + { + if (unauthorized) { + return ; + } else { + return ; + } + }} + /> + ); +}; + +export default ProtectedRoute; diff --git a/frontend/src/component/common/index.js b/frontend/src/component/common/index.js index f0e1cc8e3e..68f4b6da43 100644 --- a/frontend/src/component/common/index.js +++ b/frontend/src/component/common/index.js @@ -17,7 +17,8 @@ import ConditionallyRender from './ConditionallyRender/ConditionallyRender'; export { styles }; -export const shorten = (str, len = 50) => (str && str.length > len ? `${str.substring(0, len)}...` : str); +export const shorten = (str, len = 50) => + str && str.length > len ? `${str.substring(0, len)}...` : str; export const AppsLinkList = ({ apps }) => ( ( primary={ {appName} @@ -69,7 +73,11 @@ DataTableHeader.propTypes = { actions: PropTypes.any, }; -export const FormButtons = ({ submitText = 'Create', onCancel, primaryButtonTestId }) => ( +export const FormButtons = ({ + submitText = 'Create', + onCancel, + primaryButtonTestId, +}) => (
    @@ -220,7 +220,7 @@ exports[`renders correctly with one feature without permissions 1`] = ` />
    @@ -243,28 +243,28 @@ exports[`renders correctly with one feature without permissions 1`] = ` className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded" >

    Feature toggles

      ({ actionsContainer: { display: 'flex', alignItems: 'center', @@ -11,4 +11,12 @@ export const useStyles = makeStyles({ searchBarContainer: { marginBottom: '2rem', }, -}); + emptyStateListItem: { + border: `2px dashed ${theme.palette.borders.main}`, + padding: '0.8rem', + textAlign: 'center', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }, +})); diff --git a/frontend/src/component/feature/FeatureView/FeatureView.jsx b/frontend/src/component/feature/FeatureView/FeatureView.jsx index 8e5a5c9b54..2a224ba8fd 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.jsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.jsx @@ -2,16 +2,26 @@ import React, { useEffect, useLayoutEffect, useState } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; -import { Paper, Typography, Button, Switch, LinearProgress } from '@material-ui/core'; +import { + Paper, + Typography, + Button, + Switch, + LinearProgress, +} from '@material-ui/core'; -import HistoryComponent from '../../history/history-list-toggle-container'; +import HistoryComponent from '../../history/FeatureEventHistory'; import MetricComponent from '../view/metric-container'; import UpdateStrategies from '../view/update-strategies-container'; import EditVariants from '../variant/update-variant-container'; import FeatureTypeSelect from '../feature-type-select-container'; import ProjectSelect from '../project-select-container'; import UpdateDescriptionComponent from '../view/update-description-component'; -import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../../permissions'; +import { + CREATE_FEATURE, + DELETE_FEATURE, + UPDATE_FEATURE, +} from '../../../permissions'; import StatusComponent from '../status-component'; import FeatureTagComponent from '../feature-tag-component'; import StatusUpdateComponent from '../view/status-update-component'; @@ -70,7 +80,13 @@ const FeatureView = ({ switch (key) { case 'activation': if (isFeatureView && hasPermission(UPDATE_FEATURE)) { - return ; + return ( + + ); } return ( ; default: - return null + return null; } }; const getTabData = () => [ @@ -199,9 +215,13 @@ const FeatureView = ({ const tabs = getTabData(); - const findActiveTab = activeTab => tabs.findIndex(tab => tab.name === activeTab); + const findActiveTab = activeTab => + tabs.findIndex(tab => tab.name === activeTab); return ( - +
      @@ -209,7 +229,12 @@ const FeatureView = ({
      -
      +
      - +   - +
      toggleFeature(!featureToggle.enabled, featureToggle.name)} + onChange={() => + toggleFeature( + !featureToggle.enabled, + featureToggle.name + ) + } /> - {featureToggle.enabled ? 'Enabled' : 'Disabled'} + + {featureToggle.enabled + ? 'Enabled' + : 'Disabled'} + } elseShow={ <> - - {featureToggle.enabled ? 'Enabled' : 'Disabled'} + + + {featureToggle.enabled + ? 'Enabled' + : 'Disabled'} + } /> @@ -257,8 +307,13 @@ const FeatureView = ({ condition={isFeatureView} show={
      - - + + diff --git a/frontend/src/component/feature/strategy/AddStrategy/AddStrategy.styles.js b/frontend/src/component/feature/strategy/AddStrategy/AddStrategy.styles.js index 10b03b30ec..21aacfc46b 100644 --- a/frontend/src/component/feature/strategy/AddStrategy/AddStrategy.styles.js +++ b/frontend/src/component/feature/strategy/AddStrategy/AddStrategy.styles.js @@ -4,7 +4,6 @@ export const useStyles = makeStyles(theme => ({ createStrategyCardContainer: { display: 'flex', flexWrap: 'wrap', - justifyContent: 'center', '& > *': { marginRight: '0.5rem', marginTop: '0.5rem', diff --git a/frontend/src/component/feature/strategy/strategies-add.jsx b/frontend/src/component/feature/strategy/strategies-add.jsx index 5d7d86cb96..8bcedd1796 100644 --- a/frontend/src/component/feature/strategy/strategies-add.jsx +++ b/frontend/src/component/feature/strategy/strategies-add.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { MenuItem } from '@material-ui/core'; -import DropdownMenu from '../../common/dropdown-menu'; +import DropdownMenu from '../../common/DropdownMenu/DropdownMenu'; import styles from './strategy.module.scss'; @@ -29,7 +29,9 @@ class AddStrategy extends React.Component { addStrategy(strategyName) { const featureToggleName = this.props.featureToggleName; - const selectedStrategy = this.props.strategies.find(s => s.name === strategyName); + const selectedStrategy = this.props.strategies.find( + s => s.name === strategyName + ); const parameters = {}; selectedStrategy.parameters.forEach(({ name }) => { @@ -52,7 +54,11 @@ class AddStrategy extends React.Component { this.props.strategies .filter(s => !s.deprecated) .map(s => ( - this.addStrategy(s.name)}> + this.addStrategy(s.name)} + > {s.name} )); diff --git a/frontend/src/component/feature/variant/AddVariant/AddVariant.jsx b/frontend/src/component/feature/variant/AddVariant/AddVariant.jsx new file mode 100644 index 0000000000..a83fec6a09 --- /dev/null +++ b/frontend/src/component/feature/variant/AddVariant/AddVariant.jsx @@ -0,0 +1,299 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + FormControl, + FormControlLabel, + Grid, + Icon, + Switch, + TextField, + InputAdornment, + Button, +} from '@material-ui/core'; +import Dialog from '../../../common/Dialogue'; +import MySelect from '../../../common/select'; +import { modalStyles, trim } from '../../../common/util'; +import { weightTypes } from '../enums'; +import OverrideConfig from './OverrideConfig/OverrideConfig'; +import { useCommonStyles } from '../../../../common.styles'; + +const payloadOptions = [ + { key: 'string', label: 'string' }, + { key: 'json', label: 'json' }, + { key: 'csv', label: 'csv' }, +]; + +const EMPTY_PAYLOAD = { type: 'string', value: '' }; + +const AddVariant = ({ + showDialog, + closeDialog, + save, + validateName, + editVariant, + title, +}) => { + const [data, setData] = useState({}); + const [payload, setPayload] = useState(EMPTY_PAYLOAD); + const [overrides, setOverrides] = useState([]); + const [error, setError] = useState({}); + const commonStyles = useCommonStyles(); + + const clear = () => { + if (editVariant) { + setData({ + name: editVariant.name, + weight: editVariant.weight / 10, + weightType: editVariant.weightType || weightTypes.VARIABLE, + }); + if (editVariant.payload) { + setPayload(editVariant.payload); + } + if (editVariant.overrides) { + setOverrides(editVariant.overrides); + } else { + setOverrides([]); + } + } else { + setData({}); + setPayload(EMPTY_PAYLOAD); + setOverrides([]); + } + setError({}); + }; + + useEffect(() => { + clear(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editVariant]); + + const setVariantValue = e => { + const { name, value } = e.target; + setData({ + ...data, + [name]: trim(value), + }); + }; + + const setVariantWeightType = e => { + const { checked, name } = e.target; + const weightType = checked ? weightTypes.FIX : weightTypes.VARIABLE; + setData({ + ...data, + [name]: weightType, + }); + }; + + const submit = async e => { + e.preventDefault(); + + const validationError = validateName(data.name); + + if (validationError) { + setError(validationError); + return; + } + + try { + const variant = { + name: data.name, + weight: data.weight * 10, + weightType: data.weightType, + payload: payload.value ? payload : undefined, + overrides: overrides + .map(o => ({ + contextName: o.contextName, + values: o.values, + })) + .filter(o => o.values && o.values.length > 0), + }; + await save(variant); + clear(); + closeDialog(); + } catch (error) { + const msg = error.message || 'Could not add variant'; + setError({ general: msg }); + } + }; + + const onPayload = e => { + e.preventDefault(); + setPayload({ + ...payload, + [e.target.name]: e.target.value, + }); + }; + + const onCancel = e => { + e.preventDefault(); + clear(); + closeDialog(); + }; + + const updateOverrideType = index => e => { + e.preventDefault(); + setOverrides( + overrides.map((o, i) => { + if (i === index) { + o[e.target.name] = e.target.value; + } + return o; + }) + ); + }; + + const updateOverrideValues = (index, values) => { + setOverrides( + overrides.map((o, i) => { + if (i === index) { + o.values = values; + } + return o; + }) + ); + }; + + const removeOverride = index => e => { + e.preventDefault(); + setOverrides(overrides.filter((o, i) => i !== index)); + }; + + const onAddOverride = e => { + e.preventDefault(); + setOverrides([ + ...overrides, + ...[{ contextName: 'userId', values: [] }], + ]); + }; + + const isFixWeight = data.weightType === weightTypes.FIX; + + return ( + +
      +

      {error.general}

      + +
      + + + + % + + ), + }} + style={{ marginRight: '0.8rem' }} + value={data.weight} + error={error.weight} + type="number" + disabled={!isFixWeight} + onChange={setVariantValue} + /> + + + + + } + label="Custom percentage" + /> + + + +

      + Payload + +

      + + + + + + + + + {overrides.length > 0 && ( +

      + Overrides + +

      + )} + + + + +
      + ); +}; + +AddVariant.propTypes = { + showDialog: PropTypes.bool.isRequired, + closeDialog: PropTypes.func.isRequired, + save: PropTypes.func.isRequired, + validateName: PropTypes.func.isRequired, + editVariant: PropTypes.object, + title: PropTypes.string, +}; + +export default AddVariant; diff --git a/frontend/src/component/feature/variant/e-override-config.jsx b/frontend/src/component/feature/variant/AddVariant/OverrideConfig/OverrideConfig.jsx similarity index 66% rename from frontend/src/component/feature/variant/e-override-config.jsx rename to frontend/src/component/feature/variant/AddVariant/OverrideConfig/OverrideConfig.jsx index 76ec5e5323..5aa035f5e1 100644 --- a/frontend/src/component/feature/variant/e-override-config.jsx +++ b/frontend/src/component/feature/variant/AddVariant/OverrideConfig/OverrideConfig.jsx @@ -1,14 +1,24 @@ import { connect } from 'react-redux'; +import classnames from 'classnames'; -import React from 'react'; import PropTypes from 'prop-types'; import { Grid, IconButton, Icon } from '@material-ui/core'; -import MySelect from '../../common/select'; -import InputListField from '../../common/input-list-field'; -import { selectStyles } from '../../common'; -import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; +import MySelect from '../../../../common/select'; +import InputListField from '../../../../common/input-list-field'; +import { selectStyles } from '../../../../common'; +import ConditionallyRender from '../../../../common/ConditionallyRender/ConditionallyRender'; +import { useCommonStyles } from '../../../../../common.styles'; +import { useStyles } from './OverrideConfig.styles.js'; -function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, removeOverride, contextDefinitions }) { +const OverrideConfig = ({ + overrides, + updateOverrideType, + updateOverrideValues, + removeOverride, + contextDefinitions, +}) => { + const styles = useStyles(); + const commonStyles = useCommonStyles(); const contextNames = contextDefinitions.map(c => ({ key: c.name, label: c.name, @@ -22,24 +32,30 @@ function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, r updateOverrideValues(i, values ? values.map(v => v.value) : undefined); }; - const mapSelectValues = (values = []) => values.map(v => ({ label: v, value: v })); + 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 legalValues = + contextDefinitions.find(c => c.name === o.contextName) + .legalValues || []; const options = legalValues.map(v => ({ value: v, label: v, key: v })); return ( - - + + - + 0} show={ @@ -47,6 +63,7 @@ function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, r @@ -73,7 +90,7 @@ function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, r ); }); -} +}; OverrideConfig.propTypes = { overrides: PropTypes.array.isRequired, diff --git a/frontend/src/component/feature/variant/AddVariant/OverrideConfig/OverrideConfig.styles.js b/frontend/src/component/feature/variant/AddVariant/OverrideConfig/OverrideConfig.styles.js new file mode 100644 index 0000000000..8025622536 --- /dev/null +++ b/frontend/src/component/feature/variant/AddVariant/OverrideConfig/OverrideConfig.styles.js @@ -0,0 +1,7 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + contextFieldSelect: { + marginRight: '8px', + }, +})); diff --git a/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap b/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap index 6d20cd1859..218dc2dfce 100644 --- a/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap +++ b/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap @@ -9,7 +9,7 @@ exports[`renders correctly with with variants 1`] = ` } >

      Variants allows you to return a variant object if the feature toggle is considered enabled for the current request. When using variants you should use the @@ -473,10 +473,10 @@ exports[`renders correctly with with variants 1`] = `

      Stickiness @@ -519,7 +519,7 @@ exports[`renders correctly with without variants 1`] = ` } >

      Variants allows you to return a variant object if the feature toggle is considered enabled for the current request. When using variants you should use the @@ -577,7 +577,7 @@ exports[`renders correctly with without variants and no permissions 1`] = ` } >

      Variants allows you to return a variant object if the feature toggle is considered enabled for the current request. When using variants you should use the diff --git a/frontend/src/component/feature/variant/__tests__/update-variant-component-test.jsx b/frontend/src/component/feature/variant/__tests__/update-variant-component-test.jsx index aaed724194..f2219efd55 100644 --- a/frontend/src/component/feature/variant/__tests__/update-variant-component-test.jsx +++ b/frontend/src/component/feature/variant/__tests__/update-variant-component-test.jsx @@ -1,27 +1,33 @@ -import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { ThemeProvider } from '@material-ui/core'; import UpdateVariant from './../update-variant-component'; import renderer from 'react-test-renderer'; import { UPDATE_FEATURE } from '../../../../permissions'; import { weightTypes } from '../enums'; +import theme from '../../../../themes/main-theme'; -jest.mock('../e-override-config', () => 'OverrideConfig'); +jest.mock( + '../AddVariant/OverrideConfig/OverrideConfig.jsx', + () => 'OverrideConfig' +); test('renders correctly with without variants', () => { const tree = renderer.create( - - permission === UPDATE_FEATURE} - /> - + + + permission === UPDATE_FEATURE} + /> + + ); expect(tree).toMatchSnapshot(); @@ -29,18 +35,20 @@ test('renders correctly with without variants', () => { test('renders correctly with without variants and no permissions', () => { const tree = renderer.create( - - false} - /> - + + + false} + /> + + ); expect(tree).toMatchSnapshot(); @@ -87,18 +95,20 @@ test('renders correctly with with variants', () => { createdAt: '2018-02-04T20:27:52.127Z', }; const tree = renderer.create( - - permission === UPDATE_FEATURE} - /> - + + + permission === UPDATE_FEATURE} + /> + + ); expect(tree).toMatchSnapshot(); diff --git a/frontend/src/component/feature/variant/add-variant.jsx b/frontend/src/component/feature/variant/add-variant.jsx deleted file mode 100644 index ff6f011efd..0000000000 --- a/frontend/src/component/feature/variant/add-variant.jsx +++ /dev/null @@ -1,262 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { FormControl, FormControlLabel, Grid, Icon, Switch, TextField } from '@material-ui/core'; -import Dialog from '../../common/Dialogue'; -import MySelect from '../../common/select'; -import { modalStyles, trim } from '../../common/util'; -import { weightTypes } from './enums'; -import OverrideConfig from './e-override-config'; - -const payloadOptions = [ - { key: 'string', label: 'string' }, - { key: 'json', label: 'json' }, - { key: 'csv', label: 'csv' }, -]; - -const EMPTY_PAYLOAD = { type: 'string', value: '' }; - -function AddVariant({ showDialog, closeDialog, save, validateName, editVariant, title }) { - const [data, setData] = useState({}); - const [payload, setPayload] = useState(EMPTY_PAYLOAD); - const [overrides, setOverrides] = useState([]); - const [error, setError] = useState({}); - - const clear = () => { - if (editVariant) { - setData({ - name: editVariant.name, - weight: editVariant.weight / 10, - weightType: editVariant.weightType || weightTypes.VARIABLE, - }); - if (editVariant.payload) { - setPayload(editVariant.payload); - } - if (editVariant.overrides) { - setOverrides(editVariant.overrides); - } else { - setOverrides([]); - } - } else { - setData({}); - setPayload(EMPTY_PAYLOAD); - setOverrides([]); - } - setError({}); - }; - - useEffect(() => { - clear(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editVariant]); - - const setVariantValue = e => { - const { name, value } = e.target; - setData({ - ...data, - [name]: trim(value), - }); - }; - - const setVariantWeightType = e => { - const { checked, name } = e.target; - const weightType = checked ? weightTypes.FIX : weightTypes.VARIABLE; - setData({ - ...data, - [name]: weightType, - }); - }; - - const submit = async e => { - e.preventDefault(); - - const validationError = validateName(data.name); - - if (validationError) { - setError(validationError); - return; - } - - try { - const variant = { - name: data.name, - weight: data.weight * 10, - weightType: data.weightType, - payload: payload.value ? payload : undefined, - overrides: overrides - .map(o => ({ - contextName: o.contextName, - values: o.values, - })) - .filter(o => o.values && o.values.length > 0), - }; - await save(variant); - clear(); - closeDialog(); - } catch (error) { - const msg = error.message || 'Could not add variant'; - setError({ general: msg }); - } - }; - - const onPayload = e => { - e.preventDefault(); - setPayload({ - ...payload, - [e.target.name]: e.target.value, - }); - }; - - const onCancel = e => { - e.preventDefault(); - clear(); - closeDialog(); - }; - - const updateOverrideType = index => e => { - e.preventDefault(); - setOverrides( - overrides.map((o, i) => { - if (i === index) { - o[e.target.name] = e.target.value; - } - return o; - }) - ); - }; - - const updateOverrideValues = (index, values) => { - setOverrides( - overrides.map((o, i) => { - if (i === index) { - o.values = values; - } - return o; - }) - ); - }; - - const removeOverride = index => e => { - e.preventDefault(); - setOverrides(overrides.filter((o, i) => i !== index)); - }; - - const onAddOverride = e => { - e.preventDefault(); - setOverrides([...overrides, ...[{ contextName: 'userId', values: [] }]]); - }; - - const isFixWeight = data.weightType === weightTypes.FIX; - - return ( -

      - <> -

      {title}

      - -
      -

      {error.general}

      - -
      - - - - % - - - - - } - label="Custom percentage" - /> - - - -

      - Payload - -

      - - - - - - - - - {overrides.length > 0 && ( -

      - Overrides - -

      - )} - - - - Add override - - - -
      - ); -} - -AddVariant.propTypes = { - showDialog: PropTypes.bool.isRequired, - closeDialog: PropTypes.func.isRequired, - save: PropTypes.func.isRequired, - validateName: PropTypes.func.isRequired, - editVariant: PropTypes.object, - title: PropTypes.string, -}; - -export default AddVariant; diff --git a/frontend/src/component/feature/variant/override-config.jsx b/frontend/src/component/feature/variant/override-config.jsx deleted file mode 100644 index 45c0f51a63..0000000000 --- a/frontend/src/component/feature/variant/override-config.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Grid, IconButton, Icon } from '@material-ui/core'; -import MySelect from '../../common/select'; -import InputListField from '../../common/input-list-field'; - -const overrideOptions = [ - { key: 'userId', label: 'userId' }, - { key: 'appName', label: 'appName' }, -]; - -function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, removeOverride }) { - const updateValues = i => values => { - updateOverrideValues(i, values); - }; - - return overrides.map((o, i) => ( - - - - - - - - - - delete - - - - )); -} - -OverrideConfig.propTypes = { - overrides: PropTypes.array.isRequired, - updateOverrideType: PropTypes.func.isRequired, - updateOverrideValues: PropTypes.func.isRequired, - removeOverride: PropTypes.func.isRequired, -}; - -export default OverrideConfig; diff --git a/frontend/src/component/feature/variant/update-variant-component.jsx b/frontend/src/component/feature/variant/update-variant-component.jsx index b503c08b81..1fb2c79d13 100644 --- a/frontend/src/component/feature/variant/update-variant-component.jsx +++ b/frontend/src/component/feature/variant/update-variant-component.jsx @@ -5,8 +5,16 @@ import classnames from 'classnames'; import VariantViewComponent from './variant-view-component'; import styles from './variant.module.scss'; import { UPDATE_FEATURE } from '../../../permissions'; -import { Table, TableHead, TableRow, TableCell, TableBody, Button } from '@material-ui/core'; -import AddVariant from './add-variant'; +import { + Table, + TableHead, + TableRow, + TableCell, + TableBody, + Button, + Typography, +} from '@material-ui/core'; +import AddVariant from './AddVariant/AddVariant'; import MySelect from '../../common/select'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; @@ -103,15 +111,25 @@ class UpdateVariantComponent extends Component { return (
      - +    - By overriding the stickiness you can control which parameter you want to be used in order to ensure - consistent traffic allocation across variants.{' '} - + By overriding the stickiness you can control which parameter + you want to be used in order to ensure consistent traffic + allocation across variants.{' '} + Read more @@ -122,15 +140,19 @@ class UpdateVariantComponent extends Component { render() { const { showDialog, editVariant, editIndex, title } = this.state; const { variants, addVariant, updateVariant } = this.props; - const saveVariant = editVariant ? updateVariant.bind(null, editIndex) : addVariant; + const saveVariant = editVariant + ? updateVariant.bind(null, editIndex) + : addVariant; return (
      -

      - Variants allows you to return a variant object if the feature toggle is considered enabled for the - current request. When using variants you should use the{' '} - getVariant() method in the Client SDK. -

      + + Variants allows you to return a variant object if the + feature toggle is considered enabled for the current + request. When using variants you should use the{' '} + getVariant() method + in the Client SDK. + 0} diff --git a/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap b/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap index 44a911cf23..82e78b2622 100644 --- a/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap +++ b/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap @@ -139,10 +139,10 @@ exports[`renders correctly with one feature 1`] = `
      Project @@ -174,7 +174,7 @@ exports[`renders correctly with one feature 1`] = ` >
      diff --git a/frontend/src/component/feature/view/status-update-component.jsx b/frontend/src/component/feature/view/status-update-component.jsx index 5de8955c85..c5a1fd8c92 100644 --- a/frontend/src/component/feature/view/status-update-component.jsx +++ b/frontend/src/component/feature/view/status-update-component.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { MenuItem } from '@material-ui/core'; -import DropdownMenu from '../../common/dropdown-menu'; +import DropdownMenu from '../../common/DropdownMenu/DropdownMenu'; import PropTypes from 'prop-types'; export default function StatusUpdateComponent({ stale, updateStale }) { @@ -31,7 +31,7 @@ export default function StatusUpdateComponent({ stale, updateStale }) { renderOptions={renderOptions} id="feature-stale-dropdown" label={stale ? 'STALE' : 'ACTIVE'} - style={{ fontWeight: 'bold' }} + style={{ fontWeight: '500' }} /> ); } diff --git a/frontend/src/component/history/EventHistory/EventHistory.tsx b/frontend/src/component/history/EventHistory/EventHistory.tsx new file mode 100644 index 0000000000..3821f8cb1d --- /dev/null +++ b/frontend/src/component/history/EventHistory/EventHistory.tsx @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; + +import EventLog from '../EventLog'; + +interface IEventLogProps { + fetchHistory: () => void; + history: History; +} + +const EventHistory = ({ fetchHistory, history }: IEventLogProps) => { + useEffect(() => { + fetchHistory(); + }, [fetchHistory]); + + if (history.length < 0) { + return null; + } + + return ; +}; + +export default EventHistory; diff --git a/frontend/src/component/history/EventHistory/index.js b/frontend/src/component/history/EventHistory/index.js new file mode 100644 index 0000000000..2cbef8ad75 --- /dev/null +++ b/frontend/src/component/history/EventHistory/index.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import EventHistory from './EventHistory'; +import { fetchHistory } from '../../../store/history/actions'; + +const mapStateToProps = state => { + const history = state.history.get('list').toArray(); + return { + history, + }; +}; + +const EventHistoryContainer = connect(mapStateToProps, { fetchHistory })( + EventHistory +); + +export default EventHistoryContainer; diff --git a/frontend/src/component/history/EventLog/EventCard/EventCard.jsx b/frontend/src/component/history/EventLog/EventCard/EventCard.jsx new file mode 100644 index 0000000000..da65100c6f --- /dev/null +++ b/frontend/src/component/history/EventLog/EventCard/EventCard.jsx @@ -0,0 +1,38 @@ +import EventDiff from './EventDiff/EventDiff'; + +import { useStyles } from './EventCard.styles'; + +const EventCard = ({ entry, timeFormatted }) => { + const styles = useStyles(); + + const getName = name => { + if (name) { + return ( + <> +
      Name:
      +
      {name}
      + + ); + } else { + return null; + } + }; + + return ( +
      +
      +
      Changed at:
      +
      {timeFormatted}
      +
      Changed by:
      +
      {entry.createdBy}
      +
      Type:
      +
      {entry.type}
      + {getName(entry.data.name)} +
      + Change + +
      + ); +}; + +export default EventCard; diff --git a/frontend/src/component/history/EventLog/EventCard/EventCard.styles.js b/frontend/src/component/history/EventLog/EventCard/EventCard.styles.js new file mode 100644 index 0000000000..e0c850404d --- /dev/null +++ b/frontend/src/component/history/EventLog/EventCard/EventCard.styles.js @@ -0,0 +1,7 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles({ + eventLogHeader: { + minWidth: '110px', + }, +}); diff --git a/frontend/src/component/history/EventLog/EventCard/EventDiff/EventDiff.jsx b/frontend/src/component/history/EventLog/EventCard/EventDiff/EventDiff.jsx new file mode 100644 index 0000000000..033df96757 --- /dev/null +++ b/frontend/src/component/history/EventLog/EventCard/EventDiff/EventDiff.jsx @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; + +import { useStyles } from './EventDiff.styles'; + +const DIFF_PREFIXES = { + A: ' ', + E: ' ', + D: '-', + N: '+', +}; + +const EventDiff = ({ entry }) => { + const styles = useStyles(); + + const KLASSES = { + A: styles.blue, // array edited + E: styles.blue, // edited + D: styles.negative, // deleted + N: styles.positive, // added + }; + + const buildItemDiff = (diff, key) => { + let change; + if (diff.lhs !== undefined) { + change = ( +
      +
      + - {key}: {JSON.stringify(diff.lhs)} +
      +
      + ); + } else if (diff.rhs !== undefined) { + change = ( +
      +
      + + {key}: {JSON.stringify(diff.rhs)} +
      +
      + ); + } + + return change; + }; + + const buildDiff = (diff, idx) => { + let change; + const key = diff.path.join('.'); + + if (diff.item) { + change = buildItemDiff(diff.item, key); + } else if (diff.lhs !== undefined && diff.rhs !== undefined) { + change = ( +
      +
      + - {key}: {JSON.stringify(diff.lhs)} +
      +
      + + {key}: {JSON.stringify(diff.rhs)} +
      +
      + ); + } else { + const spadenClass = KLASSES[diff.kind]; + const prefix = DIFF_PREFIXES[diff.kind]; + + change = ( +
      + {prefix} {key}: {JSON.stringify(diff.rhs || diff.item)} +
      + ); + } + + return
      {change}
      ; + }; + + let changes; + + if (entry.diffs) { + changes = entry.diffs.map(buildDiff); + } else { + // Just show the data if there is no diff yet. + changes = ( +
      + {JSON.stringify(entry.data, null, 2)} +
      + ); + } + + return ( +
      +            
      +                {changes.length === 0 ? '(no changes)' : changes}
      +            
      +        
      + ); +}; + +EventDiff.propTypes = { + entry: PropTypes.object, +}; + +export default EventDiff; diff --git a/frontend/src/component/history/EventLog/EventCard/EventDiff/EventDiff.styles.js b/frontend/src/component/history/EventLog/EventCard/EventDiff/EventDiff.styles.js new file mode 100644 index 0000000000..1bddfdb6f1 --- /dev/null +++ b/frontend/src/component/history/EventLog/EventCard/EventDiff/EventDiff.styles.js @@ -0,0 +1,13 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + blue: { + color: theme.palette.code.edited, + }, + negative: { + color: theme.palette.code.diffSub, + }, + positive: { + color: theme.palette.code.diffAdd, + }, +})); diff --git a/frontend/src/component/history/EventLog/EventJson/EventJson.jsx b/frontend/src/component/history/EventLog/EventJson/EventJson.jsx new file mode 100644 index 0000000000..95c816d699 --- /dev/null +++ b/frontend/src/component/history/EventLog/EventJson/EventJson.jsx @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; + +import { useStyles } from './EventJson.styles'; + +const EventJson = ({ entry }) => { + const styles = useStyles(); + + const localEventData = JSON.parse(JSON.stringify(entry)); + delete localEventData.description; + delete localEventData.name; + delete localEventData.diffs; + + const prettyPrinted = JSON.stringify(localEventData, null, 2); + + return ( +
      +
      + {prettyPrinted} +
      +
      + ); +}; + +EventJson.propTypes = { + entry: PropTypes.object, +}; + +export default EventJson; diff --git a/frontend/src/component/history/EventLog/EventJson/EventJson.styles.js b/frontend/src/component/history/EventLog/EventJson/EventJson.styles.js new file mode 100644 index 0000000000..b74cd5d46f --- /dev/null +++ b/frontend/src/component/history/EventLog/EventJson/EventJson.styles.js @@ -0,0 +1,10 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + historyItem: { + padding: '5px', + '&:nth-child(odd)': { + backgroundColor: theme.palette.code.background, + }, + }, +})); diff --git a/frontend/src/component/history/EventLog/EventLog.jsx b/frontend/src/component/history/EventLog/EventLog.jsx new file mode 100644 index 0000000000..8207cb59cb --- /dev/null +++ b/frontend/src/component/history/EventLog/EventLog.jsx @@ -0,0 +1,93 @@ +import { List, Switch, FormControlLabel } from '@material-ui/core'; +import PropTypes from 'prop-types'; + +import { formatFullDateTimeWithLocale } from '../../common/util'; + +import EventJson from './EventJson/EventJson'; +import PageContent from '../../common/PageContent/PageContent'; +import HeaderTitle from '../../common/HeaderTitle'; +import EventCard from './EventCard/EventCard'; + +import { useStyles } from './EventLog.styles.js'; + +const EventLog = ({ + updateSetting, + title, + history, + settings, + displayInline, + location, + hideName, +}) => { + const styles = useStyles(); + const toggleShowDiff = () => { + updateSetting('showData', !settings.showData); + }; + const formatFulldateTime = v => { + return formatFullDateTimeWithLocale(v, location.locale); + }; + + const showData = settings.showData; + + if (!history || history.length < 0) { + return null; + } + + let entries; + + const renderListItemCards = entry => ( +
      + +
      + ); + + if (showData) { + entries = history.map(entry => ( + + )); + } else { + entries = history.map(renderListItemCards); + } + + return ( + + } + label="Full events" + /> + } + /> + } + > +
      + {entries} +
      +
      + ); +}; + +EventLog.propTypes = { + updateSettings: PropTypes.func, + title: PropTypes.string, + settings: PropTypes.object, + displayInline: PropTypes.bool, + location: PropTypes.object, + hideName: PropTypes.bool, +}; + +export default EventLog; diff --git a/frontend/src/component/history/EventLog/EventLog.styles.js b/frontend/src/component/history/EventLog/EventLog.styles.js new file mode 100644 index 0000000000..cb134d44a8 --- /dev/null +++ b/frontend/src/component/history/EventLog/EventLog.styles.js @@ -0,0 +1,40 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + eventEntry: { + border: theme.borders.default, + padding: '1rem', + margin: '1rem 0', + borderRadius: theme.borders.radius.main, + }, + history: { + '& code': { + wordWrap: 'break-word', + whiteSpace: 'pre', + fontFamily: 'monospace', + lineHeight: '100%', + color: theme.palette.code.main, + }, + '& code > .diff-N': { + color: theme.palette.code.diffAdd, + }, + '& code > .diff-D': { + color: theme.palette.code.diffSub, + }, + '& code > .diff-A, .diff-E': { + color: theme.palette.code.diffNeutral, + }, + '& dl': { + padding: '0', + }, + '& dt': { + float: 'left', + clear: 'left', + fontWeight: 'bold', + }, + '& dd': { + margin: '0 0 0 83px', + padding: '0 0 0.5em 0', + }, + }, +})); diff --git a/frontend/src/component/history/history-list-container.jsx b/frontend/src/component/history/EventLog/index.jsx similarity index 53% rename from frontend/src/component/history/history-list-container.jsx rename to frontend/src/component/history/EventLog/index.jsx index 5cbfb3d388..99ff051978 100644 --- a/frontend/src/component/history/history-list-container.jsx +++ b/frontend/src/component/history/EventLog/index.jsx @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; -import HistoryListToggleComponent from './history-list-component'; -import { updateSettingForGroup } from '../../store/settings/actions'; +import EventLog from './EventLog'; +import { updateSettingForGroup } from '../../../store/settings/actions'; const mapStateToProps = state => { const settings = state.settings.toJS().history || {}; @@ -11,8 +11,8 @@ const mapStateToProps = state => { }; }; -const HistoryListContainer = connect(mapStateToProps, { +const EventLogContainer = connect(mapStateToProps, { updateSetting: updateSettingForGroup('history'), -})(HistoryListToggleComponent); +})(EventLog); -export default HistoryListContainer; +export default EventLogContainer; diff --git a/frontend/src/component/history/FeatureEventHistory/FeatureEventHistory.jsx b/frontend/src/component/history/FeatureEventHistory/FeatureEventHistory.jsx new file mode 100644 index 0000000000..6b767ae72c --- /dev/null +++ b/frontend/src/component/history/FeatureEventHistory/FeatureEventHistory.jsx @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import { useEffect } from 'react'; +import EventLog from '../EventLog'; + +const FeatureEventHistory = ({ + toggleName, + history, + fetchHistoryForToggle, +}) => { + useEffect(() => { + fetchHistoryForToggle(toggleName); + }, [fetchHistoryForToggle, toggleName]); + + if (!history || history.length === 0) { + return fetching..; + } + + return ( + + ); +}; + +FeatureEventHistory.propTypes = { + toggleName: PropTypes.string.isRequired, + history: PropTypes.array, + fetchHistoryForToggle: PropTypes.func.isRequired, +}; + +export default FeatureEventHistory; diff --git a/frontend/src/component/history/history-list-toggle-container.jsx b/frontend/src/component/history/FeatureEventHistory/index.jsx similarity index 60% rename from frontend/src/component/history/history-list-toggle-container.jsx rename to frontend/src/component/history/FeatureEventHistory/index.jsx index b3f940e63a..797151f55b 100644 --- a/frontend/src/component/history/history-list-toggle-container.jsx +++ b/frontend/src/component/history/FeatureEventHistory/index.jsx @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; -import HistoryListToggleComponent from './history-list-toggle-component'; -import { fetchHistoryForToggle } from '../..//store/history/actions'; +import FeatureEventHistory from './FeatureEventHistory'; +import { fetchHistoryForToggle } from '../../../store/history/actions'; function getHistoryFromToggle(state, toggleName) { if (!toggleName) { @@ -18,8 +18,8 @@ const mapStateToProps = (state, props) => ({ history: getHistoryFromToggle(state, props.toggleName), }); -const HistoryListToggleContainer = connect(mapStateToProps, { +const FeatureEventHistoryContainer = connect(mapStateToProps, { fetchHistoryForToggle, -})(HistoryListToggleComponent); +})(FeatureEventHistory); -export default HistoryListToggleContainer; +export default FeatureEventHistoryContainer; diff --git a/frontend/src/component/history/history-component.jsx b/frontend/src/component/history/history-component.jsx deleted file mode 100644 index 9c6ec59a23..0000000000 --- a/frontend/src/component/history/history-component.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { Card } from '@material-ui/core'; -import HistoryList from './history-list-container'; -import { styles as commonStyles } from '../common'; - -class History extends PureComponent { - static propTypes = { - fetchHistory: PropTypes.func.isRequired, - history: PropTypes.array.isRequired, - }; - - componentDidMount() { - this.props.fetchHistory(); - } - - toggleShowDiff() { - this.setState({ showData: !this.state.showData }); - } - - render() { - const { history } = this.props; - if (history.length < 0) { - return; - } - - return ( - - - - ); - } -} -export default History; diff --git a/frontend/src/component/history/history-container.js b/frontend/src/component/history/history-container.js deleted file mode 100644 index abec5cb89a..0000000000 --- a/frontend/src/component/history/history-container.js +++ /dev/null @@ -1,14 +0,0 @@ -import { connect } from 'react-redux'; -import HistoryComponent from './history-component'; -import { fetchHistory } from './../..//store/history/actions'; - -const mapStateToProps = state => { - const history = state.history.get('list').toArray(); - return { - history, - }; -}; - -const HistoryListContainer = connect(mapStateToProps, { fetchHistory })(HistoryComponent); - -export default HistoryListContainer; diff --git a/frontend/src/component/history/history-item-diff.jsx b/frontend/src/component/history/history-item-diff.jsx deleted file mode 100644 index 64e82efcc1..0000000000 --- a/frontend/src/component/history/history-item-diff.jsx +++ /dev/null @@ -1,98 +0,0 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; - -import style from './history.module.scss'; - -const DIFF_PREFIXES = { - A: ' ', - E: ' ', - D: '-', - N: '+', -}; - -const KLASSES = { - A: style.blue, // array edited - E: style.blue, // edited - D: style.negative, // deleted - N: style.positive, // added -}; - -function buildItemDiff(diff, key) { - let change; - if (diff.lhs !== undefined) { - change = ( -
      -
      - - {key}: {JSON.stringify(diff.lhs)} -
      -
      - ); - } else if (diff.rhs !== undefined) { - change = ( -
      -
      - + {key}: {JSON.stringify(diff.rhs)} -
      -
      - ); - } - - return change; -} - -function buildDiff(diff, idx) { - let change; - const key = diff.path.join('.'); - - if (diff.item) { - change = buildItemDiff(diff.item, key); - } else if (diff.lhs !== undefined && diff.rhs !== undefined) { - change = ( -
      -
      - - {key}: {JSON.stringify(diff.lhs)} -
      -
      - + {key}: {JSON.stringify(diff.rhs)} -
      -
      - ); - } else { - const spadenClass = KLASSES[diff.kind]; - const prefix = DIFF_PREFIXES[diff.kind]; - - change = ( -
      - {prefix} {key}: {JSON.stringify(diff.rhs || diff.item)} -
      - ); - } - - return
      {change}
      ; -} - -class HistoryItem extends PureComponent { - static propTypes = { - entry: PropTypes.object, - }; - - render() { - const entry = this.props.entry; - let changes; - - if (entry.diffs) { - changes = entry.diffs.map(buildDiff); - } else { - // Just show the data if there is no diff yet. - changes =
      {JSON.stringify(entry.data, null, 2)}
      ; - } - - return ( -
      -                {changes.length === 0 ? '(no changes)' : changes}
      -            
      - ); - } -} - -export default HistoryItem; diff --git a/frontend/src/component/history/history-item-json.jsx b/frontend/src/component/history/history-item-json.jsx deleted file mode 100644 index 13aa8c1297..0000000000 --- a/frontend/src/component/history/history-item-json.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; - -import style from './history.module.scss'; - -class HistoryItem extends PureComponent { - static propTypes = { - entry: PropTypes.object, - }; - - render() { - const localEventData = JSON.parse(JSON.stringify(this.props.entry)); - delete localEventData.description; - delete localEventData.name; - delete localEventData.diffs; - - const prettyPrinted = JSON.stringify(localEventData, null, 2); - - return ( -
      -
      - {prettyPrinted} -
      -
      - ); - } -} - -export default HistoryItem; diff --git a/frontend/src/component/history/history-list-component.jsx b/frontend/src/component/history/history-list-component.jsx deleted file mode 100644 index c999e3d3de..0000000000 --- a/frontend/src/component/history/history-list-component.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import HistoryItemDiff from './history-item-diff'; -import HistoryItemJson from './history-item-json'; -import { List, Switch, FormControlLabel } from '@material-ui/core'; - -import { formatFullDateTimeWithLocale } from '../common/util'; - -import styles from './history.module.scss'; -import PageContent from '../common/PageContent/PageContent'; -import HeaderTitle from '../common/HeaderTitle'; - -const getName = name => { - if (name) { - return ( - -
      Name:
      -
      {name}
      -
      - ); - } else { - return null; - } -}; - -const HistoryMeta = ({ entry, timeFormatted }) => ( -
      -
      -
      Changed at:
      -
      {timeFormatted}
      -
      Changed by:
      -
      {entry.createdBy}
      -
      Type:
      -
      {entry.type}
      - {getName(entry.data.name)} -
      - Change - -
      -); -HistoryMeta.propTypes = { - entry: PropTypes.object.isRequired, - timeFormatted: PropTypes.string.isRequired, -}; - -class HistoryList extends Component { - static propTypes = { - title: PropTypes.string, - history: PropTypes.array, - settings: PropTypes.object, - location: PropTypes.object, - updateSetting: PropTypes.func.isRequired, - hideName: PropTypes.bool, - }; - - toggleShowDiff() { - this.props.updateSetting('showData', !this.props.settings.showData); - } - formatFulldateTime(v) { - return formatFullDateTimeWithLocale(v, this.props.location.locale); - } - render() { - const showData = this.props.settings.showData; - const { history } = this.props; - if (!history || history.length < 0) { - return null; - } - - let entries; - - const renderListItemCards = entry => ( -
      - -
      - ); - - if (showData) { - entries = history.map(entry => ); - } else { - entries = history.map(renderListItemCards); - } - - return ( - - } - label="Full events" - /> - } - /> - } - > -
      - {entries} -
      -
      - ); - } -} -export default HistoryList; diff --git a/frontend/src/component/history/history-list-toggle-component.jsx b/frontend/src/component/history/history-list-toggle-component.jsx deleted file mode 100644 index ec8e7d6684..0000000000 --- a/frontend/src/component/history/history-list-toggle-component.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import HistoryList from './history-list-container'; - -class HistoryListToggle extends Component { - static propTypes = { - toggleName: PropTypes.string.isRequired, - history: PropTypes.array, - fetchHistoryForToggle: PropTypes.func.isRequired, - }; - - componentDidMount() { - this.props.fetchHistoryForToggle(this.props.toggleName); - } - - render() { - if (!this.props.history || this.props.history.length === 0) { - return fetching..; - } - const { history } = this.props; - return ; - } -} - -export default HistoryListToggle; diff --git a/frontend/src/component/history/history.module.scss b/frontend/src/component/history/history.module.scss deleted file mode 100644 index 28dbfa71f1..0000000000 --- a/frontend/src/component/history/history.module.scss +++ /dev/null @@ -1,64 +0,0 @@ -.history { - code { - word-wrap: break-word; - white-space: pre; - font-family: monospace; - line-height: 100%; - color: #0b8c8f; - } - - code > .diff-N { - color: green; - } - - code > .diff-D { - color: red; - } - - code > .diff-A, - .diff-E { - color: black; - } - - .negative { - color: red; - } - - .positive { - color: green; - } - - .blue { - color: blue; - } - - dl { - padding: 0em; - } - - dt { - float: left; - clear: left; - font-weight: bold; - } - - dd { - margin: 0 0 0 83px; - padding: 0 0 0.5em 0; - } -} - -.history-item:nth-child(odd) { - background-color: #efefef; -} - -.history-item { - padding: 5px; -} - -.eventEntry { - border: 1px solid #f1f1f1; - padding: 1rem; - margin: 1rem 0; - border-radius: 3px; -} diff --git a/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx b/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx new file mode 100644 index 0000000000..7a9517ef6f --- /dev/null +++ b/frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx @@ -0,0 +1,16 @@ +import ConditionallyRender from '../../common/ConditionallyRender'; +import MainLayout from '../MainLayout/MainLayout'; + +const LayoutPicker = ({ children, location }) => { + const isLoginPage = location.pathname.includes('login'); + + return ( + {children}} + /> + ); +}; + +export default LayoutPicker; diff --git a/frontend/src/component/layout/main.jsx b/frontend/src/component/layout/MainLayout/MainLayout.jsx similarity index 56% rename from frontend/src/component/layout/main.jsx rename to frontend/src/component/layout/MainLayout/MainLayout.jsx index 4154241749..1181e96cab 100644 --- a/frontend/src/component/layout/main.jsx +++ b/frontend/src/component/layout/MainLayout/MainLayout.jsx @@ -2,27 +2,21 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; -import { Grid, Container } from '@material-ui/core'; +import { Grid } from '@material-ui/core'; -import styles from '../styles.module.scss'; -import ErrorContainer from '../error/error-container'; -import Header from '../menu/Header'; -import Footer from '../menu/Footer/Footer'; +import styles from '../../styles.module.scss'; +import ErrorContainer from '../../error/error-container'; +import Header from '../../menu/Header'; +import Footer from '../../menu/Footer/Footer'; const useStyles = makeStyles(theme => ({ - footer: { - background: theme.palette.neutral.main, - padding: '2rem 4rem', - color: '#fff', - width: '100%', - }, container: { height: '100%', justifyContent: 'space-between', }, })); -const Layout = ({ children, location }) => { +const MainLayout = ({ children, location }) => { const muiStyles = useStyles(); return ( @@ -31,22 +25,20 @@ const Layout = ({ children, location }) => {
      -
      {children}
      +
      + {children} +
      -
      - -
      - -
      +
      ); }; -Layout.propTypes = { +MainLayout.propTypes = { location: PropTypes.object.isRequired, }; -export default Layout; +export default MainLayout; diff --git a/frontend/src/component/menu/Footer/Footer.jsx b/frontend/src/component/menu/Footer/Footer.jsx index 35dd4b7ade..4781061393 100644 --- a/frontend/src/component/menu/Footer/Footer.jsx +++ b/frontend/src/component/menu/Footer/Footer.jsx @@ -1,52 +1,22 @@ import React from 'react'; -import { NavLink } from 'react-router-dom'; import { List, ListItem, ListItemText, Grid } from '@material-ui/core'; -import { baseRoutes as routes } from '../../menu/routes'; -import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; + import ShowApiDetailsContainer from '../../api/show-api-details-container'; -import styles from './Footer.module.scss'; +import { useStyles } from './Footer.styles'; -export const Footer = () => ( - -
      +export const Footer = () => { + const styles = useStyles(); + + return ( +
      - -
      -

      Menu

      - - 0} - show={routes.map(route => ( - - - {route.title} - - } - /> - - ))} - /> - - - GitHub - - } - /> - - -
      -

      Client SDKs

      - + ( } /> - + ( } /> - {' '} - + + + Go } /> {' '} - + ( } /> {' '} - + ( } /> - + ( } /> - + + All client SDKs } @@ -132,7 +108,7 @@ export const Footer = () => (
      - -); + ); +}; export default Footer; diff --git a/frontend/src/component/menu/Footer/Footer.module.scss b/frontend/src/component/menu/Footer/Footer.module.scss index 5da48880e9..eb9445b61f 100644 --- a/frontend/src/component/menu/Footer/Footer.module.scss +++ b/frontend/src/component/menu/Footer/Footer.module.scss @@ -10,5 +10,12 @@ .listItem a { text-decoration: none; - color: #fff; -} \ No newline at end of file + color: #000; +} + +.footer { + background: #fff; + padding: 2rem 4rem; + color: #000; + width: 100%; +} diff --git a/frontend/src/component/menu/Footer/Footer.styles.js b/frontend/src/component/menu/Footer/Footer.styles.js new file mode 100644 index 0000000000..4d3d86d18d --- /dev/null +++ b/frontend/src/component/menu/Footer/Footer.styles.js @@ -0,0 +1,21 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + footer: { + background: theme.palette.footer.background, + padding: '2rem 4rem', + width: '100%', + }, + list: { + padding: 0, + margin: 0, + }, + listItem: { + padding: 0, + margin: 0, + '& a': { + textDecoration: 'none', + color: theme.palette.footer.main, + }, + }, +})); 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 3b6e312d63..5f3d30f5c6 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/footer-test.jsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/footer-test.jsx.snap @@ -1,5 +1,341 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render DrawerMenu 1`] = `
      `; +exports[`should render DrawerMenu 1`] = ` + +`; -exports[`should render DrawerMenu with "features" selected 1`] = `
      `; +exports[`should render DrawerMenu with "features" selected 1`] = ` + +`; diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap index aaf9357ef7..bcc21a42e7 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap @@ -5,71 +5,93 @@ Array [ Object { "component": [Function], "icon": "list", + "layout": "main", "path": "/features", "title": "Feature Toggles", + "type": "protected", }, Object { "component": [Function], "icon": "extension", + "layout": "main", "path": "/strategies", "title": "Strategies", + "type": "protected", }, Object { "component": [Function], "icon": "history", + "layout": "main", "path": "/history", "title": "Event History", + "type": "protected", }, Object { "component": [Function], "icon": "archive", + "layout": "main", "path": "/archive", "title": "Archived Toggles", + "type": "protected", }, Object { "component": [Function], "icon": "apps", + "layout": "main", "path": "/applications", "title": "Applications", + "type": "protected", }, Object { "component": [Function], "flag": "C", "icon": "album", + "layout": "main", "path": "/context", "title": "Context Fields", + "type": "protected", }, Object { "component": [Function], "flag": "P", "icon": "folder_open", + "layout": "main", "path": "/projects", "title": "Projects", + "type": "protected", }, Object { "component": [Function], "icon": "label", + "layout": "main", "path": "/tag-types", "title": "Tag types", + "type": "protected", }, Object { "component": [Function], "hidden": false, "icon": "device_hub", + "layout": "main", "path": "/addons", "title": "Addons", + "type": "protected", }, Object { "component": [Function], "icon": "report", + "layout": "main", "path": "/reporting", "title": "Reporting", + "type": "protected", }, Object { "component": [Function], "icon": "exit_to_app", + "layout": "main", "path": "/logout", "title": "Sign out", + "type": "protected", }, ] `; @@ -78,212 +100,294 @@ exports[`returns all defined routes 1`] = ` Array [ Object { "component": [Function], + "layout": "main", "parent": "/features", "path": "/features/create", "title": "Create", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/features", "path": "/features/copy/:copyToggle", "title": "Copy", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/features", "path": "/features/:activeTab/:name", "title": ":name", + "type": "protected", }, Object { "component": [Function], "icon": "list", + "layout": "main", "path": "/features", "title": "Feature Toggles", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/strategies", "path": "/strategies/create", "title": "Create", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/strategies", "path": "/strategies/:activeTab/:strategyName", "title": ":strategyName", + "type": "protected", }, Object { "component": [Function], "icon": "extension", + "layout": "main", "path": "/strategies", "title": "Strategies", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/history", "path": "/history/:toggleName", "title": ":toggleName", + "type": "protected", }, Object { "component": [Function], "icon": "history", + "layout": "main", "path": "/history", "title": "Event History", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/archive", "path": "/archive/:activeTab/:name", "title": ":name", + "type": "protected", }, Object { "component": [Function], "icon": "archive", + "layout": "main", "path": "/archive", "title": "Archived Toggles", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/applications", "path": "/applications/:name", "title": ":name", + "type": "protected", }, Object { "component": [Function], "icon": "apps", + "layout": "main", "path": "/applications", "title": "Applications", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/context", "path": "/context/create", "title": "Create", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/context", "path": "/context/edit/:name", "title": ":name", + "type": "protected", }, Object { "component": [Function], "flag": "C", "icon": "album", + "layout": "main", "path": "/context", "title": "Context Fields", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/projects", "path": "/projects/create", "title": "Create", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/projects", "path": "/projects/edit/:id", "title": ":id", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/projects", "path": "/projects/:id/access", "title": ":id", + "type": "protected", }, Object { "component": [Function], "flag": "P", "icon": "folder_open", + "layout": "main", "path": "/projects", "title": "Projects", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/admin", "path": "/admin/api", "title": "API access", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/admin", "path": "/admin/users", "title": "Users", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/admin", "path": "/admin/auth", "title": "Authentication", + "type": "protected", }, Object { "component": [Function], "hidden": true, "icon": "album", + "layout": "main", "path": "/admin", "title": "Admin", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/tag-types", "path": "/tag-types/create", "title": "Create", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/tag-types", "path": "/tag-types/edit/:name", "title": ":name", + "type": "protected", }, Object { "component": [Function], "icon": "label", + "layout": "main", "path": "/tag-types", "title": "Tag types", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/tags", "path": "/tags/create", "title": "Create", + "type": "protected", }, Object { "component": [Function], "hidden": true, "icon": "label", + "layout": "main", "path": "/tags", "title": "Tags", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/addons", "path": "/addons/create/:provider", "title": "Create", + "type": "protected", }, Object { "component": [Function], + "layout": "main", "parent": "/addons", "path": "/addons/edit/:id", "title": "Edit", + "type": "protected", }, Object { "component": [Function], "hidden": false, "icon": "device_hub", + "layout": "main", "path": "/addons", "title": "Addons", + "type": "protected", }, Object { "component": [Function], "icon": "report", + "layout": "main", "path": "/reporting", "title": "Reporting", + "type": "protected", }, Object { "component": [Function], "icon": "exit_to_app", + "layout": "main", "path": "/logout", "title": "Sign out", + "type": "protected", + }, + Object { + "component": Object { + "$$typeof": Symbol(react.memo), + "WrappedComponent": [Function], + "compare": null, + "type": [Function], + }, + "hidden": true, + "icon": "user", + "layout": "standalone", + "path": "/login", + "title": "Log in", + "type": "unprotected", }, ] `; diff --git a/frontend/src/component/menu/__tests__/footer-test.jsx b/frontend/src/component/menu/__tests__/footer-test.jsx index 29895cb293..abdef2572e 100644 --- a/frontend/src/component/menu/__tests__/footer-test.jsx +++ b/frontend/src/component/menu/__tests__/footer-test.jsx @@ -1,16 +1,34 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router-dom'; +import { ThemeProvider } from '@material-ui/core'; +import { Provider } from 'react-redux'; +import { createStore } from 'redux'; import Footer from '../Footer/Footer'; +import theme from '../../../themes/main-theme'; -jest.mock('@material-ui/core'); +const mockStore = { + uiConfig: { + toJS: () => ({ + flags: { + P: true, + }, + }), + }, +}; + +const mockReducer = state => state; test('should render DrawerMenu', () => { const tree = renderer.create( - -
      - + + + +
      + + + ); expect(tree).toMatchSnapshot(); @@ -18,9 +36,13 @@ test('should render DrawerMenu', () => { test('should render DrawerMenu with "features" selected', () => { const tree = renderer.create( - -
      - + + + +
      + + + ); expect(tree).toMatchSnapshot(); diff --git a/frontend/src/component/menu/__tests__/routes-test.jsx b/frontend/src/component/menu/__tests__/routes-test.jsx index 4ef380b9bf..ef62a7490e 100644 --- a/frontend/src/component/menu/__tests__/routes-test.jsx +++ b/frontend/src/component/menu/__tests__/routes-test.jsx @@ -1,7 +1,7 @@ import { routes, baseRoutes, getRoute } from '../routes'; test('returns all defined routes', () => { - expect(routes.length).toEqual(34); + expect(routes.length).toEqual(35); expect(routes).toMatchSnapshot(); }); diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js index 74d68ef239..06c9937b82 100644 --- a/frontend/src/component/menu/routes.js +++ b/frontend/src/component/menu/routes.js @@ -32,6 +32,7 @@ import AdminApi from '../../page/admin/api'; import AdminUsers from '../../page/admin/users'; import AdminAuth from '../../page/admin/auth'; import Reporting from '../../page/reporting'; +import Login from '../user/Login'; import { P, C } from '../common/flags'; export const routes = [ @@ -41,24 +42,32 @@ export const routes = [ parent: '/features', title: 'Create', component: CreateFeatureToggle, + type: 'protected', + layout: 'main', }, { path: '/features/copy/:copyToggle', parent: '/features', title: 'Copy', component: CopyFeatureToggle, + type: 'protected', + layout: 'main', }, { path: '/features/:activeTab/:name', parent: '/features', title: ':name', component: ViewFeatureToggle, + type: 'protected', + layout: 'main', }, { path: '/features', title: 'Feature Toggles', icon: 'list', component: Features, + type: 'protected', + layout: 'main', }, // Strategies @@ -67,18 +76,24 @@ export const routes = [ title: 'Create', parent: '/strategies', component: CreateStrategies, + type: 'protected', + layout: 'main', }, { path: '/strategies/:activeTab/:strategyName', title: ':strategyName', parent: '/strategies', component: StrategyView, + type: 'protected', + layout: 'main', }, { path: '/strategies', title: 'Strategies', icon: 'extension', component: Strategies, + type: 'protected', + layout: 'main', }, // History @@ -87,12 +102,16 @@ export const routes = [ title: ':toggleName', parent: '/history', component: HistoryTogglePage, + type: 'protected', + layout: 'main', }, { path: '/history', title: 'Event History', icon: 'history', component: HistoryPage, + type: 'protected', + layout: 'main', }, // Archive @@ -101,12 +120,16 @@ export const routes = [ title: ':name', parent: '/archive', component: ShowArchive, + type: 'protected', + layout: 'main', }, { path: '/archive', title: 'Archived Toggles', icon: 'archive', component: Archive, + type: 'protected', + layout: 'main', }, // Applications @@ -115,12 +138,16 @@ export const routes = [ title: ':name', parent: '/applications', component: ApplicationView, + type: 'protected', + layout: 'main', }, { path: '/applications', title: 'Applications', icon: 'apps', component: Applications, + type: 'protected', + layout: 'main', }, // Context @@ -129,19 +156,25 @@ export const routes = [ parent: '/context', title: 'Create', component: CreateContextField, + type: 'protected', + layout: 'main', }, { path: '/context/edit/:name', parent: '/context', title: ':name', component: EditContextField, + type: 'protected', + layout: 'main', }, { path: '/context', title: 'Context Fields', icon: 'album', component: ContextFields, + type: 'protected', flag: C, + layout: 'main', }, // Project @@ -150,18 +183,24 @@ export const routes = [ parent: '/projects', title: 'Create', component: CreateProject, + type: 'protected', + layout: 'main', }, { path: '/projects/edit/:id', parent: '/projects', title: ':id', component: EditProject, + type: 'protected', + layout: 'main', }, { path: '/projects/:id/access', parent: '/projects', title: ':id', component: EditProjectAccess, + type: 'protected', + layout: 'main', }, { @@ -170,6 +209,8 @@ export const routes = [ icon: 'folder_open', component: ListProjects, flag: P, + type: 'protected', + layout: 'main', }, // Admin @@ -178,18 +219,24 @@ export const routes = [ parent: '/admin', title: 'API access', component: AdminApi, + type: 'protected', + layout: 'main', }, { path: '/admin/users', parent: '/admin', title: 'Users', component: AdminUsers, + type: 'protected', + layout: 'main', }, { path: '/admin/auth', parent: '/admin', title: 'Authentication', component: AdminAuth, + type: 'protected', + layout: 'main', }, { path: '/admin', @@ -197,6 +244,8 @@ export const routes = [ icon: 'album', component: Admin, hidden: true, + type: 'protected', + layout: 'main', }, { @@ -204,18 +253,24 @@ export const routes = [ parent: '/tag-types', title: 'Create', component: CreateTagType, + type: 'protected', + layout: 'main', }, { path: '/tag-types/edit/:name', parent: '/tag-types', title: ':name', component: EditTagType, + type: 'protected', + layout: 'main', }, { path: '/tag-types', title: 'Tag types', icon: 'label', component: ListTagTypes, + type: 'protected', + layout: 'main', }, { @@ -223,6 +278,8 @@ export const routes = [ parent: '/tags', title: 'Create', component: CreateTag, + type: 'protected', + layout: 'main', }, { path: '/tags', @@ -230,6 +287,8 @@ export const routes = [ icon: 'label', component: ListTags, hidden: true, + type: 'protected', + layout: 'main', }, // Addons @@ -238,12 +297,16 @@ export const routes = [ parent: '/addons', title: 'Create', component: AddonsCreate, + type: 'protected', + layout: 'main', }, { path: '/addons/edit/:id', parent: '/addons', title: 'Edit', component: AddonsEdit, + type: 'protected', + layout: 'main', }, { path: '/addons', @@ -251,21 +314,38 @@ export const routes = [ icon: 'device_hub', component: Addons, hidden: false, + type: 'protected', + layout: 'main', }, { path: '/reporting', title: 'Reporting', icon: 'report', component: Reporting, + type: 'protected', + layout: 'main', }, { path: '/logout', title: 'Sign out', icon: 'exit_to_app', component: LogoutFeatures, + type: 'protected', + layout: 'main', + }, + { + path: '/login', + title: 'Log in', + icon: 'user', + component: Login, + type: 'unprotected', + hidden: true, + layout: 'standalone', }, ]; export const getRoute = path => routes.find(route => route.path === path); -export const baseRoutes = routes.filter(route => !route.hidden).filter(route => !route.parent); +export const baseRoutes = routes + .filter(route => !route.hidden) + .filter(route => !route.parent); diff --git a/frontend/src/component/user/Login/Login.jsx b/frontend/src/component/user/Login/Login.jsx new file mode 100644 index 0000000000..ee9dd8c06b --- /dev/null +++ b/frontend/src/component/user/Login/Login.jsx @@ -0,0 +1,61 @@ +import { useEffect } from 'react'; +import classnames from 'classnames'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import { useTheme } from '@material-ui/core/styles'; +import { Typography } from '@material-ui/core'; + +import AuthenticationContainer from '../authentication-container'; +import ConditionallyRender from '../../common/ConditionallyRender'; + +import { ReactComponent as UnleashLogo } from '../../../icons/unleash-logo-inverted.svg'; +import { ReactComponent as SwitchesSVG } from '../../../icons/switches.svg'; +import { useStyles } from './Login.styles'; + +const Login = ({ history, loadInitialData, authDetails }) => { + const theme = useTheme(); + const styles = useStyles(); + const smallScreen = useMediaQuery(theme.breakpoints.up('md')); + + useEffect(() => { + if (!authDetails) { + loadInitialData(); + } + /* eslint-disable-next-line */ + }, []); + + return ( +
      +
      +
      +

      + Unleash +

      + + Committed to creating new ways of developing + + + +
      + } + /> +
      +
      +

      Login

      +
      + +
      +
      +
      +
      + ); +}; + +export default Login; diff --git a/frontend/src/component/user/Login/Login.styles.js b/frontend/src/component/user/Login/Login.styles.js new file mode 100644 index 0000000000..8db086628f --- /dev/null +++ b/frontend/src/component/user/Login/Login.styles.js @@ -0,0 +1,48 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + loginContainer: { + minHeight: '100vh', + width: '100%', + }, + container: { + display: 'flex', + height: '100%', + flexWrap: 'wrap', + }, + contentContainer: { + width: '50%', + padding: '4rem 3rem', + minHeight: '100vh', + [theme.breakpoints.down('sm')]: { + width: '100%', + minHeight: 'auto', + }, + }, + gradient: { + background: `linear-gradient(${theme.palette.login.gradient.top}, ${theme.palette.login.gradient.bottom})`, + color: theme.palette.login.main, + }, + title: { + fontSize: '1.5rem', + marginBottom: '0.5rem', + display: 'flex', + alignItems: 'center', + }, + logo: { + marginRight: '10px', + width: '40px', + height: '30px', + }, + subTitle: { + fontSize: '1.25rem', + }, + loginFormContainer: { + maxWidth: '300px', + }, + imageContainer: { + display: 'flex', + justifyContent: 'center', + marginTop: '8rem', + }, +})); diff --git a/frontend/src/component/user/Login/index.js b/frontend/src/component/user/Login/index.js new file mode 100644 index 0000000000..b86c0d8b2c --- /dev/null +++ b/frontend/src/component/user/Login/index.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import Login from './Login'; +import { loadInitialData } from './../../../store/loader'; + +const mapDispatchToProps = (dispatch, props) => ({ + loadInitialData: () => loadInitialData(props.flags)(dispatch), +}); + +const mapStateToProps = state => ({ + user: state.user.toJS(), + flags: state.uiConfig.toJS().flags, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Login); diff --git a/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx b/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx index 8a8b1f2980..21d5a9bdd8 100644 --- a/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx +++ b/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx @@ -1,7 +1,13 @@ import React, { useState } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; -import { CardActions, Button, TextField, Typography, IconButton } from '@material-ui/core'; +import { + CardActions, + Button, + TextField, + Typography, + IconButton, +} from '@material-ui/core'; import ConditionallyRender from '../../common/ConditionallyRender'; import { useHistory } from 'react-router'; import { useCommonStyles } from '../../../common.styles'; @@ -71,12 +77,23 @@ const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => { const { usernameError, passwordError, apiError } = errors; return ( -
      - {authDetails.message} + + + {authDetails.message} + {apiError} -
      +
      { size="small" /> -
      @@ -121,8 +143,9 @@ const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => { condition={showFields} show={renderLoginForm()} elseShow={ - onClick={onShowOptions}> - Show more options + + {' '} + onClick={onShowOptions} Show more options } /> diff --git a/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx b/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx index 101d6940bf..c92dda4788 100644 --- a/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx +++ b/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx @@ -4,7 +4,12 @@ import { Button, TextField } from '@material-ui/core'; import styles from './SimpleAuth.module.scss'; -const SimpleAuth = ({ insecureLogin, loadInitialData, history, authDetails }) => { +const SimpleAuth = ({ + insecureLogin, + loadInitialData, + history, + authDetails, +}) => { const [email, setEmail] = useState(''); const handleSubmit = evt => { @@ -27,9 +32,13 @@ const SimpleAuth = ({ insecureLogin, loadInitialData, history, authDetails }) =>

      {authDetails.message}

      - This instance of Unleash is not set up with a secure authentication provider. You can read more - about{' '} - + This instance of Unleash is not set up with a secure + authentication provider. You can read more about{' '} + securing Unleash on GitHub

      @@ -47,7 +56,13 @@ const SimpleAuth = ({ insecureLogin, loadInitialData, history, authDetails }) =>
      -
      diff --git a/frontend/src/component/user/SimpleAuth/SimpleAuth.module.scss b/frontend/src/component/user/SimpleAuth/SimpleAuth.module.scss index 131625e4e6..29b4c9d22f 100644 --- a/frontend/src/component/user/SimpleAuth/SimpleAuth.module.scss +++ b/frontend/src/component/user/SimpleAuth/SimpleAuth.module.scss @@ -1,3 +1,7 @@ .container > * { - margin: 1rem 0; -} \ No newline at end of file + margin: 0.6rem 0; +} + +.button { + min-width: 150px; +} diff --git a/frontend/src/component/user/authentication-component.jsx b/frontend/src/component/user/authentication-component.jsx index ae2a394fa5..0d9d59d7cb 100644 --- a/frontend/src/component/user/authentication-component.jsx +++ b/frontend/src/component/user/authentication-component.jsx @@ -1,15 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Dialog, Icon, DialogTitle } from '@material-ui/core'; import SimpleAuth from './SimpleAuth/SimpleAuth'; import AuthenticationCustomComponent from './authentication-custom-component'; -import AuthenticationPasswordComponent from './PasswordAuth/PasswordAuth'; +import PasswordAuth from './PasswordAuth/PasswordAuth'; const SIMPLE_TYPE = 'unsecure'; const PASSWORD_TYPE = 'password'; -const customStyles = {}; - class AuthComponent extends React.Component { static propTypes = { user: PropTypes.object.isRequired, @@ -26,7 +23,7 @@ class AuthComponent extends React.Component { let content; if (authDetails.type === PASSWORD_TYPE) { content = ( - ); } else { - content = ; + content = ( + + ); } - return ( -
      - - - - person Login - - - -
      {content}
      -
      -
      - ); + return
      {content}
      ; } } diff --git a/frontend/src/component/user/logout-component.jsx b/frontend/src/component/user/logout-component.jsx index 33d97f8d7e..2020043f68 100644 --- a/frontend/src/component/user/logout-component.jsx +++ b/frontend/src/component/user/logout-component.jsx @@ -3,22 +3,23 @@ import PropTypes from 'prop-types'; import { Card, CardContent, CardHeader } from '@material-ui/core'; import { styles as commonStyles } from '../common'; -const LogoutComponent = ({logoutUser}) => { +const LogoutComponent = ({ logoutUser, history }) => { useEffect(() => { logoutUser(); }); - return ( + return ( + Logged out - You have now been successfully logged out of Unleash. Thank you for using Unleash.{' '} + You have now been successfully logged out of Unleash. Thank you + for using Unleash.{' '} ); -} +}; LogoutComponent.propTypes = { - logoutUser: PropTypes.func.isRequired -} + logoutUser: PropTypes.func.isRequired, +}; export default LogoutComponent; - diff --git a/frontend/src/component/user/show-user-component.jsx b/frontend/src/component/user/show-user-component.jsx index 7a5eeb7c88..0f9872f9f4 100644 --- a/frontend/src/component/user/show-user-component.jsx +++ b/frontend/src/component/user/show-user-component.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styles from './user.module.scss'; import { MenuItem, Avatar, Typography, Icon } from '@material-ui/core'; -import DropdownMenu from '../common/dropdown-menu'; +import DropdownMenu from '../common/DropdownMenu/DropdownMenu'; export default class ShowUserComponent extends React.Component { static propTypes = { @@ -38,7 +38,11 @@ export default class ShowUserComponent extends React.Component { } getLocale() { - return (this.props.location && this.props.location.locale) || navigator.language || navigator.userLanguage; + return ( + (this.props.location && this.props.location.locale) || + navigator.language || + navigator.userLanguage + ); } setLocale(locale) { @@ -49,26 +53,50 @@ export default class ShowUserComponent extends React.Component { const email = this.props.profile ? this.props.profile.email : ''; const locale = this.getLocale(); let foundLocale = this.possibleLocales.find(l => l.value === locale); - const imageUrl = email ? this.props.profile.imageUrl : 'unknown-user.png'; - const imageLocale = foundLocale ? `${foundLocale.image}.png` : `unknown-locale.png`; + const imageUrl = email + ? this.props.profile.imageUrl + : 'unknown-user.png'; + const imageLocale = foundLocale + ? `${foundLocale.image}.png` + : `unknown-locale.png`; return (
      } + startIcon={ + + } renderOptions={() => this.possibleLocales.map(i => ( - this.setLocale(i)}> + this.setLocale(i)} + >
      - {i.value} - {i.value} + {i.value} + + {i.value} +
      )) } label="Locale" /> - +
      ); } diff --git a/frontend/src/icons/switches.svg b/frontend/src/icons/switches.svg new file mode 100644 index 0000000000..aad9bd6f54 --- /dev/null +++ b/frontend/src/icons/switches.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/icons/unleash-logo-inverted.svg b/frontend/src/icons/unleash-logo-inverted.svg new file mode 100644 index 0000000000..32da7185f1 --- /dev/null +++ b/frontend/src/icons/unleash-logo-inverted.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 79f74fbcae..9441593e9f 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -2,7 +2,6 @@ import 'whatwg-fetch'; import './app.css'; -import React from 'react'; import ReactDOM from 'react-dom'; import { HashRouter, Route } from 'react-router-dom'; import { Provider } from 'react-redux'; @@ -14,21 +13,26 @@ import { StylesProvider } from '@material-ui/core/styles'; import mainTheme from './themes/main-theme'; import store from './store'; import MetricsPoller from './metrics-poller'; -import App from './component/app'; +import App from './component/App'; import ScrollToTop from './component/scroll-to-top'; import { writeWarning } from './security-logger'; - let composeEnhancers; -if (process.env.NODE_ENV !== 'production' && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { +if ( + process.env.NODE_ENV !== 'production' && + (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ +) { composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; } else { composeEnhancers = compose; writeWarning(); } -const unleashStore = createStore(store, composeEnhancers(applyMiddleware(thunkMiddleware))); +const unleashStore = createStore( + store, + composeEnhancers(applyMiddleware(thunkMiddleware)) +); const metricsPoller = new MetricsPoller(unleashStore); metricsPoller.start(); diff --git a/frontend/src/interfaces/route.ts b/frontend/src/interfaces/route.ts new file mode 100644 index 0000000000..e4aee7861c --- /dev/null +++ b/frontend/src/interfaces/route.ts @@ -0,0 +1,16 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router'; + +interface IRoute { + path: string; + icon?: string; + title?: string; + component: React.ComponentType; + type: string; + layout: string; + hidden?: boolean; + flag?: string; + parent?: string; +} + +export default IRoute; diff --git a/frontend/src/interfaces/user.ts b/frontend/src/interfaces/user.ts new file mode 100644 index 0000000000..95b6647aee --- /dev/null +++ b/frontend/src/interfaces/user.ts @@ -0,0 +1,24 @@ +interface IUser { + authDetails: IAuthDetails; + showDialog: boolean; + profile?: IProfile; +} + +interface IAuthDetails { + type: string; + path: string; + message: string; + options: string[]; +} + +interface IProfile { + id: number; + createdAt: string; + imageUrl: string; + loginAttempts: number; + permissions: string[]; + seenAt: string; + username: string; +} + +export default IUser; diff --git a/frontend/src/page/history/index.js b/frontend/src/page/history/index.js index 8b2b5a8c3f..30e247616b 100644 --- a/frontend/src/page/history/index.js +++ b/frontend/src/page/history/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import HistoryComponent from '../../component/history/history-container'; +import HistoryComponent from '../../component/history/EventHistory'; const render = () => ; diff --git a/frontend/src/page/history/toggle.js b/frontend/src/page/history/toggle.js index e0146459ad..428e298826 100644 --- a/frontend/src/page/history/toggle.js +++ b/frontend/src/page/history/toggle.js @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import HistoryListToggle from '../../component/history/history-list-toggle-container'; +import HistoryListToggle from '../../component/history/FeatureEventHistory'; -const render = ({ match: { params } }) => ; +const render = ({ match: { params } }) => ( + +); render.propTypes = { match: PropTypes.object.isRequired, diff --git a/frontend/src/store/api-calls/index.js b/frontend/src/store/api-calls/index.js index 5f8b3e55fc..d748d4232a 100644 --- a/frontend/src/store/api-calls/index.js +++ b/frontend/src/store/api-calls/index.js @@ -2,6 +2,7 @@ import { START_FETCH_FEATURE_TOGGLES, FETCH_FEATURE_TOGGLES_SUCCESS, FETCH_FEATURE_TOGGLE_ERROR, + RESET_LOADING, } from '../feature-toggle/actions'; const apiCalls = ( @@ -21,10 +22,10 @@ const apiCalls = ( return { ...state, fetchTogglesState: { + ...state.fetchTogglesState, loading: true, success: false, error: null, - count: (state.fetchTogglesState.count += 1), }, }; case FETCH_FEATURE_TOGGLES_SUCCESS: @@ -35,6 +36,7 @@ const apiCalls = ( loading: false, success: true, error: null, + count: (state.fetchTogglesState.count += 1), }, }; case FETCH_FEATURE_TOGGLE_ERROR: @@ -47,6 +49,11 @@ const apiCalls = ( error: true, }, }; + case RESET_LOADING: + return { + ...state, + fetchTogglesState: { ...state.fetchTogglesState, count: 0 }, + }; default: return state; } diff --git a/frontend/src/store/feature-toggle/actions.js b/frontend/src/store/feature-toggle/actions.js index 9967daa2ab..c263d3a1e4 100644 --- a/frontend/src/store/feature-toggle/actions.js +++ b/frontend/src/store/feature-toggle/actions.js @@ -18,8 +18,10 @@ export const ERROR_FETCH_FEATURE_TOGGLES = 'ERROR_FETCH_FEATURE_TOGGLES'; export const ERROR_CREATING_FEATURE_TOGGLE = 'ERROR_CREATING_FEATURE_TOGGLE'; export const ERROR_UPDATE_FEATURE_TOGGLE = 'ERROR_UPDATE_FEATURE_TOGGLE'; export const ERROR_REMOVE_FEATURE_TOGGLE = 'ERROR_REMOVE_FEATURE_TOGGLE'; -export const UPDATE_FEATURE_TOGGLE_STRATEGIES = 'UPDATE_FEATURE_TOGGLE_STRATEGIES'; +export const UPDATE_FEATURE_TOGGLE_STRATEGIES = + 'UPDATE_FEATURE_TOGGLE_STRATEGIES'; export const FETCH_FEATURE_TOGGLE_ERROR = 'FETCH_FEATURE_TOGGLE_ERROR'; +export const RESET_LOADING = 'RESET_LOADING'; export const RECEIVE_FEATURE_TOGGLE = 'RECEIVE_FEATURE_TOGGLE'; export const START_FETCH_FEATURE_TOGGLE = 'START_FETCH_FEATURE_TOGGLE'; @@ -131,7 +133,10 @@ export function requestSetStaleFeatureToggle(stale, name) { .setStale(stale, name) .then(featureToggle => { const info = `${name} marked as ${stale ? 'Stale' : 'Active'}.`; - setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000); + setTimeout( + () => dispatch({ type: MUTE_ERROR, error: info }), + 1000 + ); dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle, info }); }) .catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE)); @@ -146,14 +151,20 @@ export function requestUpdateFeatureToggle(featureToggle) { .update(featureToggle) .then(() => { const info = `${featureToggle.name} successfully updated!`; - setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000); + setTimeout( + () => dispatch({ type: MUTE_ERROR, error: info }), + 1000 + ); dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle, info }); }) .catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE)); }; } -export function requestUpdateFeatureToggleStrategies(featureToggle, newStrategies) { +export function requestUpdateFeatureToggleStrategies( + featureToggle, + newStrategies +) { return dispatch => { featureToggle.strategies = newStrategies; dispatch({ type: START_UPDATE_FEATURE_TOGGLE }); @@ -162,7 +173,10 @@ export function requestUpdateFeatureToggleStrategies(featureToggle, newStrategie .update(featureToggle) .then(() => { const info = `${featureToggle.name} successfully updated!`; - setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000); + setTimeout( + () => dispatch({ type: MUTE_ERROR, error: info }), + 1000 + ); return dispatch({ type: UPDATE_FEATURE_TOGGLE_STRATEGIES, featureToggle, @@ -182,7 +196,10 @@ export function requestUpdateFeatureToggleVariants(featureToggle, newVariants) { .update(featureToggle) .then(() => { const info = `${featureToggle.name} successfully updated!`; - setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000); + setTimeout( + () => dispatch({ type: MUTE_ERROR, error: info }), + 1000 + ); return dispatch({ type: UPDATE_FEATURE_TOGGLE_STRATEGIES, featureToggle, @@ -199,7 +216,9 @@ export function removeFeatureToggle(featureToggleName) { return api .remove(featureToggleName) - .then(() => dispatch({ type: REMOVE_FEATURE_TOGGLE, featureToggleName })) + .then(() => + dispatch({ type: REMOVE_FEATURE_TOGGLE, featureToggleName }) + ) .catch(dispatchError(dispatch, ERROR_REMOVE_FEATURE_TOGGLE)); }; } diff --git a/frontend/src/store/user/actions.js b/frontend/src/store/user/actions.js index 108d28ecee..4487e7cb8e 100644 --- a/frontend/src/store/user/actions.js +++ b/frontend/src/store/user/actions.js @@ -1,13 +1,14 @@ -import api from "./api"; -import { dispatchError } from "../util"; -export const USER_CHANGE_CURRENT = "USER_CHANGE_CURRENT"; -export const USER_LOGOUT = "USER_LOGOUT"; -export const USER_LOGIN = "USER_LOGIN"; -export const START_FETCH_USER = "START_FETCH_USER"; -export const ERROR_FETCH_USER = "ERROR_FETCH_USER"; -const debug = require("debug")("unleash:user-actions"); +import api from './api'; +import { dispatchError } from '../util'; +import { RESET_LOADING } from '../feature-toggle/actions'; +export const USER_CHANGE_CURRENT = 'USER_CHANGE_CURRENT'; +export const USER_LOGOUT = 'USER_LOGOUT'; +export const USER_LOGIN = 'USER_LOGIN'; +export const START_FETCH_USER = 'START_FETCH_USER'; +export const ERROR_FETCH_USER = 'ERROR_FETCH_USER'; +const debug = require('debug')('unleash:user-actions'); -const updateUser = (value) => ({ +const updateUser = value => ({ type: USER_CHANGE_CURRENT, value, }); @@ -18,44 +19,44 @@ function handleError(error) { export function fetchUser() { debug('Start fetching user'); - return (dispatch) => { + return dispatch => { dispatch({ type: START_FETCH_USER }); return api .fetchUser() - .then((json) => dispatch(updateUser(json))) + .then(json => dispatch(updateUser(json))) .catch(dispatchError(dispatch, ERROR_FETCH_USER)); }; } export function insecureLogin(path, user) { - return (dispatch) => { + return dispatch => { dispatch({ type: START_FETCH_USER }); return api .insecureLogin(path, user) - .then((json) => dispatch(updateUser(json))) + .then(json => dispatch(updateUser(json))) .catch(handleError); }; } export function passwordLogin(path, user) { - return (dispatch) => { + return dispatch => { dispatch({ type: START_FETCH_USER }); return api .passwordLogin(path, user) - .then((json) => dispatch(updateUser(json))) + .then(json => dispatch(updateUser(json))) .then(() => dispatch({ type: USER_LOGIN })); }; } export function logoutUser() { - return (dispatch) => { + return dispatch => { return api .logoutUser() .then(() => dispatch({ type: USER_LOGOUT })) - .then(() => window.location = "/") + .then(() => dispatch({ type: RESET_LOADING })) .catch(handleError); }; } diff --git a/frontend/src/themes/main-theme.js b/frontend/src/themes/main-theme.js index 775db1dc2b..9132cc285b 100644 --- a/frontend/src/themes/main-theme.js +++ b/frontend/src/themes/main-theme.js @@ -4,11 +4,13 @@ const theme = createMuiTheme({ palette: { primary: { main: '#607d8b', - light: '#B2DFDB', - dark: '#00796B', + light: '#8eacbb', + dark: '#34515e', }, secondary: { - main: '#217584', + main: '#00695c', + light: '#439889', + dark: '#003d33', }, neutral: { main: '#18243e', @@ -31,6 +33,9 @@ const theme = createMuiTheme({ links: { deprecated: '#1d1818', }, + borders: { + main: '#f1f1f1', + }, error: { main: '#d95e5e', }, @@ -40,6 +45,18 @@ const theme = createMuiTheme({ division: { main: '#f1f1f1', }, + footer: { + main: '#000', + background: '#fff', + }, + code: { + main: '#0b8c8f', + diffAdd: 'green', + diffSub: 'red', + diffNeutral: 'black', + edited: 'blue', + background: '#efefef', + }, cards: { gradient: { top: '#617D8B', @@ -49,6 +66,13 @@ const theme = createMuiTheme({ bg: '#f1f1f1', }, }, + login: { + gradient: { + top: '#607D8B', + bottom: '#173341', + }, + main: '#fff', + }, }, padding: { pageContent: {