mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
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
This commit is contained in:
parent
4e92e068ab
commit
86631b53c9
7
frontend/.prettierrc
Normal file
7
frontend/.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 80
|
||||
}
|
11
frontend/public/switches.svg
Normal file
11
frontend/public/switches.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="197" height="310" viewBox="0 0 197 310" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M126.669 92.7899C134.256 92.7899 140.408 86.642 140.408 79.0581C140.408 71.4742 134.256 65.3263 126.669 65.3263C119.081 65.3263 112.93 71.4742 112.93 79.0581C112.93 86.642 119.081 92.7899 126.669 92.7899Z" fill="white"/>
|
||||
<path d="M129.417 99.6559H67.5913C62.1256 99.6559 56.8838 97.4858 53.019 93.623C49.1541 89.7601 46.9829 84.521 46.9829 79.0582C46.9829 73.5953 49.1541 68.3562 53.019 64.4934C56.8838 60.6306 62.1256 58.4604 67.5913 58.4604H129.417C134.882 58.4604 140.124 60.6306 143.989 64.4934C147.854 68.3562 150.025 73.5953 150.025 79.0582C150.025 84.521 147.854 89.7601 143.989 93.623C140.124 97.4858 134.882 99.6559 129.417 99.6559ZM67.5913 61.2068C62.8544 61.2068 58.3114 63.0876 54.9619 66.4354C51.6124 69.7831 49.7307 74.3237 49.7307 79.0582C49.7307 83.7926 51.6124 88.3332 54.9619 91.681C58.3114 95.0288 62.8544 96.9095 67.5913 96.9095H129.417C134.153 96.9095 138.696 95.0288 142.046 91.681C145.395 88.3332 147.277 83.7926 147.277 79.0582C147.277 74.3237 145.395 69.7831 142.046 66.4354C138.696 63.0876 134.153 61.2068 129.417 61.2068H67.5913Z" fill="white"/>
|
||||
<path d="M69.818 168.383C77.4058 168.383 83.5569 162.235 83.5569 154.651C83.5569 147.067 77.4058 140.919 69.818 140.919C62.2302 140.919 56.0791 147.067 56.0791 154.651C56.0791 162.235 62.2302 168.383 69.818 168.383Z" fill="white"/>
|
||||
<path d="M129.417 175.248H67.5913C62.1256 175.248 56.8838 173.078 53.019 169.216C49.1541 165.353 46.9829 160.114 46.9829 154.651C46.9829 149.188 49.1541 143.949 53.019 140.086C56.8838 136.223 62.1256 134.053 67.5913 134.053H129.417C134.882 134.053 140.124 136.223 143.989 140.086C147.854 143.949 150.025 149.188 150.025 154.651C150.025 160.114 147.854 165.353 143.989 169.216C140.124 173.078 134.882 175.248 129.417 175.248ZM67.5913 136.799C62.8544 136.799 58.3115 138.68 54.962 142.028C51.6124 145.376 49.7307 149.916 49.7307 154.651C49.7307 159.385 51.6124 163.926 54.962 167.274C58.3115 170.621 62.8544 172.502 67.5913 172.502H129.417C134.153 172.502 138.696 170.621 142.046 167.274C145.395 163.926 147.277 159.385 147.277 154.651C147.277 149.916 145.395 145.376 142.046 142.028C138.696 138.68 134.153 136.799 129.417 136.799H67.5913Z" fill="white"/>
|
||||
<path d="M69.818 243.975C77.4058 243.975 83.5569 237.827 83.5569 230.243C83.5569 222.659 77.4058 216.512 69.818 216.512C62.2302 216.512 56.0791 222.659 56.0791 230.243C56.0791 237.827 62.2302 243.975 69.818 243.975Z" fill="white"/>
|
||||
<path d="M129.417 250.841H67.5913C62.1256 250.841 56.8838 248.671 53.019 244.808C49.1541 240.945 46.9829 235.706 46.9829 230.243C46.9829 224.78 49.1541 219.541 53.019 215.679C56.8838 211.816 62.1256 209.646 67.5913 209.646H129.417C134.882 209.646 140.124 211.816 143.989 215.679C147.854 219.541 150.025 224.78 150.025 230.243C150.025 235.706 147.854 240.945 143.989 244.808C140.124 248.671 134.882 250.841 129.417 250.841ZM67.5913 212.392C62.8544 212.392 58.3114 214.273 54.9619 217.621C51.6124 220.968 49.7307 225.509 49.7307 230.243C49.7307 234.978 51.6124 239.518 54.9619 242.866C58.3114 246.214 62.8544 248.095 67.5913 248.095H129.417C131.762 248.095 134.085 247.633 136.251 246.736C138.418 245.839 140.387 244.524 142.046 242.866C143.704 241.209 145.02 239.241 145.918 237.075C146.815 234.909 147.277 232.588 147.277 230.243C147.277 227.899 146.815 225.578 145.918 223.412C145.02 221.246 143.704 219.278 142.046 217.621C140.387 215.963 138.418 214.648 136.251 213.751C134.085 212.854 131.762 212.392 129.417 212.392H67.5913Z" fill="white"/>
|
||||
<path d="M193.97 309.126H3.03846C2.23497 309.125 1.46466 308.806 0.896511 308.238C0.328359 307.67 0.00875734 306.9 0.0078125 306.097V3.20437C0.00875734 2.4013 0.328359 1.63139 0.896511 1.06353C1.46466 0.495674 2.23497 0.176237 3.03846 0.175293H193.97C194.773 0.176237 195.543 0.495674 196.111 1.06353C196.68 1.63139 196.999 2.4013 197 3.20437V306.097C196.999 306.9 196.68 307.67 196.111 308.238C195.543 308.806 194.773 309.125 193.97 309.126ZM3.03846 1.38692C2.55635 1.38745 2.09415 1.5791 1.75325 1.91982C1.41235 2.26054 1.2206 2.72251 1.22007 3.20437V306.097C1.2206 306.579 1.41235 307.041 1.75325 307.382C2.09415 307.722 2.55635 307.914 3.03846 307.915H193.97C194.452 307.914 194.914 307.722 195.255 307.382C195.596 307.041 195.787 306.579 195.788 306.097V3.20437C195.787 2.72251 195.596 2.26054 195.255 1.91982C194.914 1.5791 194.452 1.38745 193.97 1.38692H3.03846Z" fill="white"/>
|
||||
<path d="M193.969 309H3.03077C2.22725 308.999 1.45691 308.68 0.888733 308.112C0.320559 307.544 0.000944877 306.774 0 305.97V3.02956C0.000944877 2.22636 0.320559 1.45633 0.888733 0.888379C1.45691 0.320432 2.22725 0.0009445 3.03077 0H193.969C194.773 0.0009445 195.543 0.320432 196.111 0.888379C196.679 1.45633 196.999 2.22636 197 3.02956V305.97C196.999 306.774 196.679 307.544 196.111 308.112C195.543 308.68 194.773 308.999 193.969 309ZM3.03077 1.21182C2.54864 1.21235 2.08642 1.40403 1.7455 1.74481C1.40459 2.08558 1.21283 2.54763 1.21231 3.02956V305.97C1.21283 306.452 1.40459 306.914 1.7455 307.255C2.08642 307.596 2.54864 307.788 3.03077 307.788H193.969C194.451 307.788 194.914 307.596 195.255 307.255C195.595 306.914 195.787 306.452 195.788 305.97V3.02956C195.787 2.54763 195.595 2.08558 195.255 1.74481C194.914 1.40403 194.451 1.21235 193.969 1.21182H3.03077Z" fill="white"/>
|
||||
<path d="M193.969 309H3.03077C2.22725 308.999 1.45691 308.68 0.888733 308.112C0.320559 307.544 0.000944877 306.774 0 305.97V3.02956C0.000944877 2.22636 0.320559 1.45633 0.888733 0.888379C1.45691 0.320432 2.22725 0.0009445 3.03077 0H193.969C194.773 0.0009445 195.543 0.320432 196.111 0.888379C196.679 1.45633 196.999 2.22636 197 3.02956V305.97C196.999 306.774 196.679 307.544 196.111 308.112C195.543 308.68 194.773 308.999 193.969 309ZM3.03077 1.21182C2.54864 1.21235 2.08642 1.40403 1.7455 1.74481C1.40459 2.08558 1.21283 2.54763 1.21231 3.02956V305.97C1.21283 306.452 1.40459 306.914 1.7455 307.255C2.08642 307.596 2.54864 307.788 3.03077 307.788H193.969C194.451 307.788 194.914 307.596 195.255 307.255C195.595 306.914 195.787 306.452 195.788 305.97V3.02956C195.787 2.54763 195.595 2.08558 195.255 1.74481C194.914 1.40403 194.451 1.21235 193.969 1.21182H3.03077Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
@ -32,4 +32,10 @@ export const useCommonStyles = makeStyles(theme => ({
|
||||
textCenter: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
fullWidth: {
|
||||
width: '100%',
|
||||
},
|
||||
fullHeight: {
|
||||
height: '100%',
|
||||
},
|
||||
}));
|
||||
|
82
frontend/src/component/App.tsx
Normal file
82
frontend/src/component/App.tsx
Normal file
@ -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 (
|
||||
<ProtectedRoute
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
component={route.component}
|
||||
unauthorized={unauthorized}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
render={props => <route.component {...props} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<LayoutPicker location={location}>
|
||||
<Switch>
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/"
|
||||
unauthorized={isUnauthorized()}
|
||||
component={Redirect}
|
||||
renderProps={{ to: '/features' }}
|
||||
/>
|
||||
{renderMainLayoutRoutes()}
|
||||
{renderStandaloneRoutes()}
|
||||
</Switch>
|
||||
</LayoutPicker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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);
|
@ -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 => (
|
||||
<ReportToggleListItem key={feature.name} {...feature} bulkActionsOn={BULK_ACTIONS_ON} />
|
||||
<ReportToggleListItem
|
||||
key={feature.name}
|
||||
{...feature}
|
||||
bulkActionsOn={BULK_ACTIONS_ON}
|
||||
/>
|
||||
));
|
||||
|
||||
const renderBulkActionsMenu = () => (
|
||||
@ -62,7 +77,10 @@ const ReportToggleList = ({ features, selectedProject }) => {
|
||||
<Paper className={styles.reportToggleList}>
|
||||
<div className={styles.reportToggleListHeader}>
|
||||
<h3 className={styles.reportToggleListHeading}>Overview</h3>
|
||||
<ConditionallyRender condition={BULK_ACTIONS_ON} show={renderBulkActionsMenu} />
|
||||
<ConditionallyRender
|
||||
condition={BULK_ACTIONS_ON}
|
||||
show={renderBulkActionsMenu}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.reportToggleListInnerContainer}>
|
||||
<table className={styles.reportingToggleTable}>
|
||||
|
@ -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 <AuthenticationContainer history={this.props.history} />;
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<MainLayout {...this.props}>
|
||||
<Switch>
|
||||
<Route exact path="/" render={() => <Redirect to="/features" component={Features} />} />
|
||||
{routes.map(route => (
|
||||
<Route key={route.path} path={route.path} component={route.component} />
|
||||
))}
|
||||
</Switch>
|
||||
</MainLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: state.user.toJS(),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(App);
|
@ -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);
|
@ -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, {
|
||||
|
@ -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) => (
|
||||
<MenuItem disabled={selectedId === item.id} data-target={item.id} key={item.id}>
|
||||
<MenuItem
|
||||
disabled={selectedId === item.id}
|
||||
data-target={item.id}
|
||||
key={item.id}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const renderProjectOptions = () => {
|
||||
const start = [
|
||||
<MenuItem disabled={curentProject === ALL_PROJECTS} data-target={ALL_PROJECTS.id}>
|
||||
<MenuItem
|
||||
disabled={curentProject === ALL_PROJECTS}
|
||||
data-target={ALL_PROJECTS.id}
|
||||
>
|
||||
{ALL_PROJECTS.name}
|
||||
</MenuItem>,
|
||||
];
|
||||
|
||||
return [...start, ...projects.map(p => renderProjectItem(currentProjectId, p))];
|
||||
return [
|
||||
...start,
|
||||
...projects.map(p => renderProjectItem(currentProjectId, p)),
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { Route, Redirect } from 'react-router-dom';
|
||||
|
||||
const ProtectedRoute = ({
|
||||
component: Component,
|
||||
unauthorized,
|
||||
renderProps = {},
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={props => {
|
||||
if (unauthorized) {
|
||||
return <Redirect to="/login" />;
|
||||
} else {
|
||||
return <Component {...props} {...renderProps} />;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
@ -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 }) => (
|
||||
<List>
|
||||
<ConditionallyRender
|
||||
@ -38,7 +39,10 @@ export const AppsLinkList = ({ apps }) => (
|
||||
primary={
|
||||
<Link
|
||||
to={`/applications/${appName}`}
|
||||
className={[styles.listLink, styles.truncate].join(' ')}
|
||||
className={[
|
||||
styles.listLink,
|
||||
styles.truncate,
|
||||
].join(' ')}
|
||||
>
|
||||
{appName}
|
||||
</Link>
|
||||
@ -69,7 +73,11 @@ DataTableHeader.propTypes = {
|
||||
actions: PropTypes.any,
|
||||
};
|
||||
|
||||
export const FormButtons = ({ submitText = 'Create', onCancel, primaryButtonTestId }) => (
|
||||
export const FormButtons = ({
|
||||
submitText = 'Create',
|
||||
onCancel,
|
||||
primaryButtonTestId,
|
||||
}) => (
|
||||
<div>
|
||||
<Button
|
||||
data-test={primaryButtonTestId}
|
||||
@ -108,7 +116,12 @@ export function getIcon(type) {
|
||||
}
|
||||
|
||||
export const IconLink = ({ url, icon }) => (
|
||||
<a href={url} target="_blank" rel="noreferrer" className="mdl-color-text--grey-600">
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mdl-color-text--grey-600"
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</a>
|
||||
);
|
||||
@ -146,11 +159,20 @@ DropdownButton.propTypes = {
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
startIcon: PropTypes.string,
|
||||
startIcon: PropTypes.object,
|
||||
};
|
||||
|
||||
export const MenuItemWithIcon = ({ icon, label, disabled, ...menuItemProps }) => (
|
||||
<MenuItem disabled={disabled} style={{ display: 'flex', alignItems: 'center' }} {...menuItemProps}>
|
||||
export const MenuItemWithIcon = ({
|
||||
icon,
|
||||
label,
|
||||
disabled,
|
||||
...menuItemProps
|
||||
}) => (
|
||||
<MenuItem
|
||||
disabled={disabled}
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
{...menuItemProps}
|
||||
>
|
||||
<Icon style={{ paddingRight: '16px' }}>{icon}</Icon>
|
||||
{label}
|
||||
</MenuItem>
|
||||
@ -163,7 +185,11 @@ MenuItemWithIcon.propTypes = {
|
||||
|
||||
const badNumbers = [NaN, Infinity, -Infinity];
|
||||
export function calc(value, total, decimal) {
|
||||
if (typeof value !== 'number' || typeof total !== 'number' || typeof decimal !== 'number') {
|
||||
if (
|
||||
typeof value !== 'number' ||
|
||||
typeof total !== 'number' ||
|
||||
typeof decimal !== 'number'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,18 @@ import React from 'react';
|
||||
import { Select, FormControl, MenuItem, InputLabel } from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const SelectMenu = ({ name, value, label, options, onChange, id, disabled = false, className, ...rest }) => {
|
||||
const SelectMenu = ({
|
||||
name,
|
||||
value,
|
||||
label,
|
||||
options,
|
||||
onChange,
|
||||
id,
|
||||
disabled = false,
|
||||
className,
|
||||
classes,
|
||||
...rest
|
||||
}) => {
|
||||
const renderSelectItems = () =>
|
||||
options.map(option => (
|
||||
<MenuItem key={option.key} value={option.key} title={option.title}>
|
||||
@ -11,7 +22,7 @@ const SelectMenu = ({ name, value, label, options, onChange, id, disabled = fals
|
||||
));
|
||||
|
||||
return (
|
||||
<FormControl variant="outlined" size="small">
|
||||
<FormControl variant="outlined" size="small" classes={classes}>
|
||||
<InputLabel htmlFor={id} id={id}>
|
||||
{label}
|
||||
</InputLabel>
|
||||
|
@ -1,8 +1,15 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, List, Tooltip, IconButton, Icon } from '@material-ui/core';
|
||||
import {
|
||||
Button,
|
||||
List,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Icon,
|
||||
ListItem,
|
||||
} from '@material-ui/core';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
|
||||
import FeatureToggleListItem from './FeatureToggleListItem';
|
||||
@ -32,7 +39,7 @@ const FeatureToggleList = ({
|
||||
const styles = useStyles();
|
||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
fetcher();
|
||||
}, [fetcher]);
|
||||
|
||||
@ -65,18 +72,32 @@ const FeatureToggleList = ({
|
||||
));
|
||||
}
|
||||
|
||||
return features.map(feature => (
|
||||
<FeatureToggleListItem
|
||||
key={feature.name}
|
||||
settings={settings}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||
feature={feature}
|
||||
toggleFeature={toggleFeature}
|
||||
revive={revive}
|
||||
hasPermission={hasPermission}
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={features.length > 0}
|
||||
show={features.map(feature => (
|
||||
<FeatureToggleListItem
|
||||
key={feature.name}
|
||||
settings={settings}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={
|
||||
featureMetrics.lastMinute[feature.name]
|
||||
}
|
||||
feature={feature}
|
||||
toggleFeature={toggleFeature}
|
||||
revive={revive}
|
||||
hasPermission={hasPermission}
|
||||
/>
|
||||
))}
|
||||
elseShow={
|
||||
<ListItem className={styles.emptyStateListItem}>
|
||||
No features available. Get started by adding a new
|
||||
feature toggle.
|
||||
<Link to="/features/create">Add your first toggle</Link>
|
||||
</ListItem>
|
||||
}
|
||||
/>
|
||||
));
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { MenuItem } from '@material-ui/core';
|
||||
import { MenuItemWithIcon } from '../../../common';
|
||||
import DropdownMenu from '../../../common/dropdown-menu';
|
||||
import DropdownMenu from '../../../common/DropdownMenu/DropdownMenu';
|
||||
import ProjectSelect from '../../../common/ProjectSelect';
|
||||
import { useStyles } from './styles';
|
||||
import classnames from 'classnames';
|
||||
@ -19,8 +19,13 @@ const sortingOptions = [
|
||||
{ type: 'metrics', displayName: 'Metrics' },
|
||||
];
|
||||
|
||||
|
||||
const FeatureToggleListActions = ({ settings, setSort, toggleMetrics, updateSetting, loading }) => {
|
||||
const FeatureToggleListActions = ({
|
||||
settings,
|
||||
setSort,
|
||||
toggleMetrics,
|
||||
updateSetting,
|
||||
loading,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const handleSort = e => {
|
||||
@ -32,7 +37,11 @@ const FeatureToggleListActions = ({ settings, setSort, toggleMetrics, updateSett
|
||||
|
||||
const renderSortingOptions = () =>
|
||||
sortingOptions.map(option => (
|
||||
<MenuItem key={option.type} disabled={isDisabled(option.type)} data-target={option.type}>
|
||||
<MenuItem
|
||||
key={option.type}
|
||||
disabled={isDisabled(option.type)}
|
||||
data-target={option.type}
|
||||
>
|
||||
{option.displayName}
|
||||
</MenuItem>
|
||||
));
|
||||
|
@ -7,11 +7,11 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="makeStyles-search-4"
|
||||
className="makeStyles-search-5"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root makeStyles-searchIcon-5"
|
||||
className="MuiSvgIcon-root makeStyles-searchIcon-6"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
@ -20,7 +20,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
className="MuiInputBase-root makeStyles-inputRoot-6"
|
||||
className="MuiInputBase-root makeStyles-inputRoot-7"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
>
|
||||
@ -43,28 +43,28 @@ exports[`renders correctly with one feature 1`] = `
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-7"
|
||||
className="makeStyles-headerContainer-8"
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerTitleContainer-11"
|
||||
className="makeStyles-headerTitleContainer-12"
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<h2
|
||||
className="MuiTypography-root makeStyles-headerTitle-12 MuiTypography-h2"
|
||||
className="MuiTypography-root makeStyles-headerTitle-13 MuiTypography-h2"
|
||||
>
|
||||
Feature toggles
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="makeStyles-headerActions-13"
|
||||
className="makeStyles-headerActions-14"
|
||||
>
|
||||
<div
|
||||
className="makeStyles-actionsContainer-1"
|
||||
>
|
||||
<div
|
||||
className="makeStyles-actions-14"
|
||||
className="makeStyles-actions-15"
|
||||
>
|
||||
<button
|
||||
aria-controls="metric"
|
||||
@ -174,7 +174,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="makeStyles-bodyContainer-8"
|
||||
className="makeStyles-bodyContainer-9"
|
||||
>
|
||||
<ul
|
||||
className="MuiList-root MuiList-padding"
|
||||
@ -207,11 +207,11 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="makeStyles-search-4"
|
||||
className="makeStyles-search-5"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="MuiSvgIcon-root makeStyles-searchIcon-5"
|
||||
className="MuiSvgIcon-root makeStyles-searchIcon-6"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
@ -220,7 +220,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
className="MuiInputBase-root makeStyles-inputRoot-6"
|
||||
className="MuiInputBase-root makeStyles-inputRoot-7"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
>
|
||||
@ -243,28 +243,28 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerContainer-7"
|
||||
className="makeStyles-headerContainer-8"
|
||||
>
|
||||
<div
|
||||
className="makeStyles-headerTitleContainer-11"
|
||||
className="makeStyles-headerTitleContainer-12"
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<h2
|
||||
className="MuiTypography-root makeStyles-headerTitle-12 MuiTypography-h2"
|
||||
className="MuiTypography-root makeStyles-headerTitle-13 MuiTypography-h2"
|
||||
>
|
||||
Feature toggles
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="makeStyles-headerActions-13"
|
||||
className="makeStyles-headerActions-14"
|
||||
>
|
||||
<div
|
||||
className="makeStyles-actionsContainer-1"
|
||||
>
|
||||
<div
|
||||
className="makeStyles-actions-14"
|
||||
className="makeStyles-actions-15"
|
||||
>
|
||||
<button
|
||||
aria-controls="metric"
|
||||
@ -354,7 +354,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="makeStyles-bodyContainer-8"
|
||||
className="makeStyles-bodyContainer-9"
|
||||
>
|
||||
<ul
|
||||
className="MuiList-root MuiList-padding"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
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',
|
||||
},
|
||||
}));
|
||||
|
@ -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 <UpdateStrategies featureToggle={featureToggle} features={features} history={history} />;
|
||||
return (
|
||||
<UpdateStrategies
|
||||
featureToggle={featureToggle}
|
||||
features={features}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<UpdateStrategies
|
||||
@ -94,7 +110,7 @@ const FeatureView = ({
|
||||
case 'log':
|
||||
return <HistoryComponent toggleName={featureToggleName} />;
|
||||
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 (
|
||||
<Paper className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||
<Paper
|
||||
className={commonStyles.fullwidth}
|
||||
style={{ overflow: 'visible' }}
|
||||
>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<Typography variant="h1" className={styles.heading}>
|
||||
@ -209,7 +229,12 @@ const FeatureView = ({
|
||||
</Typography>
|
||||
<StatusComponent stale={featureToggle.stale} />
|
||||
</div>
|
||||
<div className={classnames(styles.featureInfoContainer, commonStyles.contentSpacingY)}>
|
||||
<div
|
||||
className={classnames(
|
||||
styles.featureInfoContainer,
|
||||
commonStyles.contentSpacingY
|
||||
)}
|
||||
>
|
||||
<UpdateDescriptionComponent
|
||||
isFeatureView={isFeatureView}
|
||||
description={featureToggle.description}
|
||||
@ -217,9 +242,18 @@ const FeatureView = ({
|
||||
hasPermission={hasPermission}
|
||||
/>
|
||||
<div className={styles.selectContainer}>
|
||||
<FeatureTypeSelect value={featureToggle.type} onChange={updateType} label="Feature type" />
|
||||
<FeatureTypeSelect
|
||||
value={featureToggle.type}
|
||||
onChange={updateType}
|
||||
label="Feature type"
|
||||
/>
|
||||
|
||||
<ProjectSelect value={featureToggle.project} onChange={updateProject} label="Project" filled />
|
||||
<ProjectSelect
|
||||
value={featureToggle.project}
|
||||
onChange={updateProject}
|
||||
label="Project"
|
||||
filled
|
||||
/>
|
||||
</div>
|
||||
<FeatureTagComponent
|
||||
featureToggleName={featureToggle.name}
|
||||
@ -239,15 +273,31 @@ const FeatureView = ({
|
||||
<Switch
|
||||
disabled={!isFeatureView}
|
||||
checked={featureToggle.enabled}
|
||||
onChange={() => toggleFeature(!featureToggle.enabled, featureToggle.name)}
|
||||
onChange={() =>
|
||||
toggleFeature(
|
||||
!featureToggle.enabled,
|
||||
featureToggle.name
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span>{featureToggle.enabled ? 'Enabled' : 'Disabled'}</span>
|
||||
<span>
|
||||
{featureToggle.enabled
|
||||
? 'Enabled'
|
||||
: 'Disabled'}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
elseShow={
|
||||
<>
|
||||
<Switch disabled checked={featureToggle.enabled} />
|
||||
<span>{featureToggle.enabled ? 'Enabled' : 'Disabled'}</span>
|
||||
<Switch
|
||||
disabled
|
||||
checked={featureToggle.enabled}
|
||||
/>
|
||||
<span>
|
||||
{featureToggle.enabled
|
||||
? 'Enabled'
|
||||
: 'Disabled'}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@ -257,8 +307,13 @@ const FeatureView = ({
|
||||
condition={isFeatureView}
|
||||
show={
|
||||
<div>
|
||||
<AddTagDialog featureToggleName={featureToggle.name} />
|
||||
<StatusUpdateComponent stale={featureToggle.stale} updateStale={updateStale} />
|
||||
<AddTagDialog
|
||||
featureToggleName={featureToggle.name}
|
||||
/>
|
||||
<StatusUpdateComponent
|
||||
stale={featureToggle.stale}
|
||||
updateStale={updateStale}
|
||||
/>
|
||||
<Button
|
||||
title="Create new feature toggle by cloning configuration"
|
||||
component={Link}
|
||||
@ -293,7 +348,11 @@ const FeatureView = ({
|
||||
|
||||
<hr />
|
||||
|
||||
<TabNav tabData={tabs} className={styles.tabContentContainer} startingTab={findActiveTab(activeTab)} />
|
||||
<TabNav
|
||||
tabData={tabs}
|
||||
className={styles.tabContentContainer}
|
||||
startingTab={findActiveTab(activeTab)}
|
||||
/>
|
||||
<ConfirmDialogue
|
||||
open={delDialog}
|
||||
title="Are you sure you want to archive this toggle"
|
||||
|
@ -1,6 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Dialog, DialogContent, DialogTitle, DialogActions, Typography } from '@material-ui/core';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import CreateStrategyCard from './AddStrategyCard/AddStrategyCard';
|
||||
import { useStyles } from './AddStrategy.styles';
|
||||
@ -8,11 +15,20 @@ import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import { resolveDefaultParamValue } from './utils';
|
||||
import { getHumanReadbleStrategy } from '../../../../utils/strategy-names';
|
||||
|
||||
const AddStrategy = ({ strategies, showCreateStrategy, setShowCreateStrategy, featureToggleName, addStrategy }) => {
|
||||
const AddStrategy = ({
|
||||
strategies,
|
||||
showCreateStrategy,
|
||||
setShowCreateStrategy,
|
||||
featureToggleName,
|
||||
addStrategy,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
if (!strategies) return null;
|
||||
|
||||
const builtInStrategies = strategies.filter(strategy => strategy.editable !== true);
|
||||
const builtInStrategies = strategies
|
||||
.filter(strategy => strategy.editable !== true)
|
||||
.filter(strategy => !strategy.deprecated);
|
||||
|
||||
const customStrategies = strategies.filter(strategy => strategy.editable);
|
||||
|
||||
const setStrategyByName = strategyName => {
|
||||
@ -20,7 +36,10 @@ const AddStrategy = ({ strategies, showCreateStrategy, setShowCreateStrategy, fe
|
||||
const parameters = {};
|
||||
|
||||
selectedStrategy.parameters.forEach(({ name }) => {
|
||||
parameters[name] = resolveDefaultParamValue(name, featureToggleName);
|
||||
parameters[name] = resolveDefaultParamValue(
|
||||
name,
|
||||
featureToggleName
|
||||
);
|
||||
});
|
||||
|
||||
addStrategy({
|
||||
@ -29,8 +48,31 @@ const AddStrategy = ({ strategies, showCreateStrategy, setShowCreateStrategy, fe
|
||||
});
|
||||
};
|
||||
|
||||
const orderStrategies = strategies => {
|
||||
const order = [
|
||||
'default',
|
||||
'flexibleRollout',
|
||||
'userWithId',
|
||||
'remoteAddress',
|
||||
'applicationHostname',
|
||||
];
|
||||
const temp = [...strategies];
|
||||
const result = [];
|
||||
|
||||
while (order.length > 0) {
|
||||
const matchValue = order[0];
|
||||
temp.forEach(value => {
|
||||
if (value.name === matchValue) {
|
||||
result.push(value);
|
||||
}
|
||||
});
|
||||
order.shift();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const renderBuiltInStrategies = () =>
|
||||
builtInStrategies.map(strategy => (
|
||||
orderStrategies(builtInStrategies).map(strategy => (
|
||||
<CreateStrategyCard
|
||||
strategy={getHumanReadbleStrategy(strategy.name)}
|
||||
key={strategy.name}
|
||||
@ -54,30 +96,45 @@ const AddStrategy = ({ strategies, showCreateStrategy, setShowCreateStrategy, fe
|
||||
));
|
||||
|
||||
return (
|
||||
<Dialog open={showCreateStrategy} aria-labelledby="form-dialog-title" fullWidth maxWidth="md">
|
||||
<Dialog
|
||||
open={showCreateStrategy}
|
||||
aria-labelledby="form-dialog-title"
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
>
|
||||
<DialogTitle id="form-dialog-title">Add a new strategy</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Typography variant="subtitle1" className={styles.subTitle}>
|
||||
Built in strategies
|
||||
</Typography>
|
||||
<div className={styles.createStrategyCardContainer}>{renderBuiltInStrategies()}</div>
|
||||
<div className={styles.createStrategyCardContainer}>
|
||||
{renderBuiltInStrategies()}
|
||||
</div>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={customStrategies.length > 0}
|
||||
show={
|
||||
<>
|
||||
<Typography variant="subtitle1" className={styles.subTitle}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
className={styles.subTitle}
|
||||
>
|
||||
Custom strategies
|
||||
</Typography>
|
||||
<div className={styles.createStrategyCardContainer}>{renderCustomStrategies()}</div>
|
||||
<div className={styles.createStrategyCardContainer}>
|
||||
{renderCustomStrategies()}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button color="secondary" onClick={() => setShowCreateStrategy(false)}>
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => setShowCreateStrategy(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
@ -4,7 +4,6 @@ export const useStyles = makeStyles(theme => ({
|
||||
createStrategyCardContainer: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
'& > *': {
|
||||
marginRight: '0.5rem',
|
||||
marginTop: '0.5rem',
|
||||
|
@ -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 => (
|
||||
<MenuItem key={s.name} title={s.description} onClick={() => this.addStrategy(s.name)}>
|
||||
<MenuItem
|
||||
key={s.name}
|
||||
title={s.description}
|
||||
onClick={() => this.addStrategy(s.name)}
|
||||
>
|
||||
{s.name}
|
||||
</MenuItem>
|
||||
));
|
||||
|
299
frontend/src/component/feature/variant/AddVariant/AddVariant.jsx
Normal file
299
frontend/src/component/feature/variant/AddVariant/AddVariant.jsx
Normal file
@ -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 (
|
||||
<Dialog
|
||||
open={showDialog}
|
||||
contentLabel="Example Modal"
|
||||
style={modalStyles}
|
||||
onClose={onCancel}
|
||||
onClick={submit}
|
||||
primaryButtonText="Save"
|
||||
secondaryButtonText="Cancel"
|
||||
title={title}
|
||||
>
|
||||
<form onSubmit={submit} className={commonStyles.contentSpacingY}>
|
||||
<p style={{ color: 'red' }}>{error.general}</p>
|
||||
<TextField
|
||||
label="Variant name"
|
||||
name="name"
|
||||
placeholder=""
|
||||
className={commonStyles.fullWidth}
|
||||
value={data.name}
|
||||
error={error.name}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
type="name"
|
||||
onChange={setVariantValue}
|
||||
/>
|
||||
<br />
|
||||
<Grid container>
|
||||
<Grid item md={4}>
|
||||
<TextField
|
||||
id="weight"
|
||||
label="Weight"
|
||||
name="weight"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
placeholder=""
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="start">
|
||||
%
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
style={{ marginRight: '0.8rem' }}
|
||||
value={data.weight}
|
||||
error={error.weight}
|
||||
type="number"
|
||||
disabled={!isFixWeight}
|
||||
onChange={setVariantValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="weightType"
|
||||
value={isFixWeight}
|
||||
onChange={setVariantWeightType}
|
||||
/>
|
||||
}
|
||||
label="Custom percentage"
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<p style={{ marginBottom: '1rem' }}>
|
||||
<strong>Payload </strong>
|
||||
<Icon
|
||||
name="info"
|
||||
title="Passed to the variant object. Can be anything (json, value, csv)"
|
||||
/>
|
||||
</p>
|
||||
<Grid container>
|
||||
<Grid item md={3}>
|
||||
<MySelect
|
||||
name="type"
|
||||
label="Type"
|
||||
className={commonStyles.fullWidth}
|
||||
value={payload.type}
|
||||
options={payloadOptions}
|
||||
onChange={onPayload}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item md={9}>
|
||||
<TextField
|
||||
rows={1}
|
||||
label="Value"
|
||||
name="value"
|
||||
className={commonStyles.fullWidth}
|
||||
value={payload.value}
|
||||
onChange={onPayload}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{overrides.length > 0 && (
|
||||
<p style={{ marginBottom: '.5rem' }}>
|
||||
<strong>Overrides </strong>
|
||||
<Icon
|
||||
name="info"
|
||||
title="Here you can specify which users that should get this variant."
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<OverrideConfig
|
||||
overrides={overrides}
|
||||
removeOverride={removeOverride}
|
||||
updateOverrideType={updateOverrideType}
|
||||
updateOverrideValues={updateOverrideValues}
|
||||
updateValues={updateOverrideValues}
|
||||
/>
|
||||
<Button onClick={onAddOverride}>Add override</Button>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
@ -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 (
|
||||
<Grid container key={`override=${i}`}>
|
||||
<Grid item md={3}>
|
||||
<Grid container key={`override=${i}`} alignItems="center">
|
||||
<Grid item md={3} className={styles.contextFieldSelect}>
|
||||
<MySelect
|
||||
name="contextName"
|
||||
label="Context Field"
|
||||
value={o.contextName}
|
||||
options={contextNames}
|
||||
classes={{
|
||||
root: classnames(commonStyles.fullWidth),
|
||||
}}
|
||||
onChange={updateOverrideType(i)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid md={8} item>
|
||||
<Grid md={7} item>
|
||||
<ConditionallyRender
|
||||
condition={legalValues && legalValues.length > 0}
|
||||
show={
|
||||
@ -47,6 +63,7 @@ function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, r
|
||||
<MySelect
|
||||
key={`override-select=${i}`}
|
||||
className={selectStyles}
|
||||
classes={{ root: commonStyles.fullWidth }}
|
||||
value={mapSelectValues(o.values)}
|
||||
options={options}
|
||||
onChange={updateSelectValues(i)}
|
||||
@ -58,7 +75,7 @@ function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, r
|
||||
label="Values (v1, v2, ...)"
|
||||
name="values"
|
||||
placeholder=""
|
||||
style={{ width: '100%' }}
|
||||
classes={{ root: commonStyles.fullWidth }}
|
||||
values={o.values}
|
||||
updateValues={updateValues(i)}
|
||||
/>
|
||||
@ -73,7 +90,7 @@ function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, r
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
OverrideConfig.propTypes = {
|
||||
overrides: PropTypes.array.isRequired,
|
@ -0,0 +1,7 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
contextFieldSelect: {
|
||||
marginRight: '8px',
|
||||
},
|
||||
}));
|
@ -9,7 +9,7 @@ exports[`renders correctly with with variants 1`] = `
|
||||
}
|
||||
>
|
||||
<p
|
||||
className="paragraph"
|
||||
className="MuiTypography-root MuiTypography-body1"
|
||||
>
|
||||
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`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-1 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-11 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-3 PrivateNotchedOutline-legendNotched-4"
|
||||
className="PrivateNotchedOutline-legendLabelled-13 PrivateNotchedOutline-legendNotched-14"
|
||||
>
|
||||
<span>
|
||||
Stickiness
|
||||
@ -519,7 +519,7 @@ exports[`renders correctly with without variants 1`] = `
|
||||
}
|
||||
>
|
||||
<p
|
||||
className="paragraph"
|
||||
className="MuiTypography-root MuiTypography-body1"
|
||||
>
|
||||
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`] = `
|
||||
}
|
||||
>
|
||||
<p
|
||||
className="paragraph"
|
||||
className="MuiTypography-root MuiTypography-body1"
|
||||
>
|
||||
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
|
||||
|
||||
|
@ -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(
|
||||
<MemoryRouter>
|
||||
<UpdateVariant
|
||||
key={0}
|
||||
variants={[]}
|
||||
addVariant={jest.fn()}
|
||||
removeVariant={jest.fn()}
|
||||
updateVariant={jest.fn()}
|
||||
stickinessOptions={['default']}
|
||||
updateStickiness={jest.fn()}
|
||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<MemoryRouter>
|
||||
<UpdateVariant
|
||||
key={0}
|
||||
variants={[]}
|
||||
addVariant={jest.fn()}
|
||||
removeVariant={jest.fn()}
|
||||
updateVariant={jest.fn()}
|
||||
stickinessOptions={['default']}
|
||||
updateStickiness={jest.fn()}
|
||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<MemoryRouter>
|
||||
<UpdateVariant
|
||||
key={0}
|
||||
variants={[]}
|
||||
addVariant={jest.fn()}
|
||||
removeVariant={jest.fn()}
|
||||
updateVariant={jest.fn()}
|
||||
stickinessOptions={['default']}
|
||||
updateStickiness={jest.fn()}
|
||||
hasPermission={() => false}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<MemoryRouter>
|
||||
<UpdateVariant
|
||||
key={0}
|
||||
variants={[]}
|
||||
addVariant={jest.fn()}
|
||||
removeVariant={jest.fn()}
|
||||
updateVariant={jest.fn()}
|
||||
stickinessOptions={['default']}
|
||||
updateStickiness={jest.fn()}
|
||||
hasPermission={() => false}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
@ -87,18 +95,20 @@ test('renders correctly with with variants', () => {
|
||||
createdAt: '2018-02-04T20:27:52.127Z',
|
||||
};
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<UpdateVariant
|
||||
key={0}
|
||||
variants={featureToggle.variants}
|
||||
addVariant={jest.fn()}
|
||||
removeVariant={jest.fn()}
|
||||
updateVariant={jest.fn()}
|
||||
stickinessOptions={['default']}
|
||||
updateStickiness={jest.fn()}
|
||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<MemoryRouter>
|
||||
<UpdateVariant
|
||||
key={0}
|
||||
variants={featureToggle.variants}
|
||||
addVariant={jest.fn()}
|
||||
removeVariant={jest.fn()}
|
||||
updateVariant={jest.fn()}
|
||||
stickinessOptions={['default']}
|
||||
updateStickiness={jest.fn()}
|
||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
@ -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 (
|
||||
<Dialog
|
||||
open={showDialog}
|
||||
contentLabel="Example Modal"
|
||||
style={modalStyles}
|
||||
onClose={onCancel}
|
||||
onClick={submit}
|
||||
primaryButtonText="Save"
|
||||
secondaryButtonText="Cancel"
|
||||
>
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<p style={{ color: 'red' }}>{error.general}</p>
|
||||
<TextField
|
||||
label="Variant name"
|
||||
name="name"
|
||||
placeholder=""
|
||||
style={{ width: '100%' }}
|
||||
value={data.name}
|
||||
error={error.name}
|
||||
type="name"
|
||||
onChange={setVariantValue}
|
||||
/>
|
||||
<br />
|
||||
<Grid container>
|
||||
<Grid item md={3}>
|
||||
<TextField
|
||||
id="weight"
|
||||
label="Weight"
|
||||
name="weight"
|
||||
placeholder=""
|
||||
value={data.weight}
|
||||
error={error.weight}
|
||||
type="number"
|
||||
disabled={!isFixWeight}
|
||||
onChange={setVariantValue}
|
||||
/>
|
||||
<span>%</span>
|
||||
</Grid>
|
||||
<Grid item md={9}>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch name="weightType" value={isFixWeight} onChange={setVariantWeightType} />
|
||||
}
|
||||
label="Custom percentage"
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<p style={{ marginBottom: '1rem' }}>
|
||||
<strong>Payload </strong>
|
||||
<Icon name="info" title="Passed to the variant object. Can be anything (json, value, csv)" />
|
||||
</p>
|
||||
<Grid container>
|
||||
<Grid item md={3}>
|
||||
<MySelect
|
||||
name="type"
|
||||
label="Type"
|
||||
style={{ width: '100%' }}
|
||||
value={payload.type}
|
||||
options={payloadOptions}
|
||||
onChange={onPayload}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item md={9}>
|
||||
<TextField
|
||||
rows={1}
|
||||
label="Value"
|
||||
name="value"
|
||||
style={{ width: '100%' }}
|
||||
value={payload.value}
|
||||
onChange={onPayload}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{overrides.length > 0 && (
|
||||
<p style={{ marginBottom: '.5rem' }}>
|
||||
<strong>Overrides </strong>
|
||||
<Icon name="info" title="Here you can specify which users that should get this variant." />
|
||||
</p>
|
||||
)}
|
||||
|
||||
<OverrideConfig
|
||||
overrides={overrides}
|
||||
removeOverride={removeOverride}
|
||||
updateOverrideType={updateOverrideType}
|
||||
updateOverrideValues={updateOverrideValues}
|
||||
updateValues={updateOverrideValues}
|
||||
/>
|
||||
<a href="#add-override" onClick={onAddOverride}>
|
||||
<small>Add override</small>
|
||||
</a>
|
||||
</form>
|
||||
</>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
@ -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) => (
|
||||
<Grid container key={`override=${i}`}>
|
||||
<Grid item md={3}>
|
||||
<MySelect
|
||||
name="contextName"
|
||||
label="Context Field"
|
||||
value={o.contextName}
|
||||
options={overrideOptions}
|
||||
onChange={updateOverrideType(i)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item md={8}>
|
||||
<InputListField
|
||||
label="Values (v1, v2, ...)"
|
||||
name="values"
|
||||
placeholder=""
|
||||
style={{ width: '100%' }}
|
||||
values={o.values}
|
||||
updateValues={updateValues(i)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item md={1}>
|
||||
<IconButton onClick={removeOverride(i)}>
|
||||
<Icon>delete</Icon>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
));
|
||||
}
|
||||
|
||||
OverrideConfig.propTypes = {
|
||||
overrides: PropTypes.array.isRequired,
|
||||
updateOverrideType: PropTypes.func.isRequired,
|
||||
updateOverrideValues: PropTypes.func.isRequired,
|
||||
removeOverride: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default OverrideConfig;
|
@ -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 (
|
||||
<section style={{ paddingTop: '16px' }}>
|
||||
<MySelect label="Stickiness" options={options} value={value} onChange={onChange} />
|
||||
<MySelect
|
||||
label="Stickiness"
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<small
|
||||
className={classnames(styles.paragraph, styles.helperText)}
|
||||
style={{ display: 'block', marginTop: '0.5rem' }}
|
||||
>
|
||||
By overriding the stickiness you can control which parameter you want to be used in order to ensure
|
||||
consistent traffic allocation across variants.{' '}
|
||||
<a href="https://unleash.github.io/docs/toggle_variants" target="_blank" rel="noreferrer">
|
||||
By overriding the stickiness you can control which parameter
|
||||
you want to be used in order to ensure consistent traffic
|
||||
allocation across variants.{' '}
|
||||
<a
|
||||
href="https://unleash.github.io/docs/toggle_variants"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Read more
|
||||
</a>
|
||||
</small>
|
||||
@ -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 (
|
||||
<section style={{ padding: '16px' }}>
|
||||
<p className={styles.paragraph}>
|
||||
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{' '}
|
||||
<code style={{ color: 'navy' }}>getVariant()</code> method in the Client SDK.
|
||||
</p>
|
||||
<Typography variant="body1">
|
||||
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{' '}
|
||||
<code style={{ color: 'navy' }}>getVariant()</code> method
|
||||
in the Client SDK.
|
||||
</Typography>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={variants.length > 0}
|
||||
|
@ -139,10 +139,10 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-9 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-11 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-11"
|
||||
className="PrivateNotchedOutline-legendLabelled-13"
|
||||
>
|
||||
<span>
|
||||
Project
|
||||
@ -174,7 +174,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-disabled={false}
|
||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-13 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-15 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||
onBlur={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
@ -193,7 +193,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="PrivateSwitchBase-input-16 MuiSwitch-input"
|
||||
className="PrivateSwitchBase-input-18 MuiSwitch-input"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
@ -235,7 +235,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": "bold",
|
||||
"fontWeight": "500",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
@ -317,7 +317,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className="MuiPaper-root makeStyles-tabNav-17 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
className="MuiPaper-root makeStyles-tabNav-19 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root"
|
||||
@ -365,7 +365,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
Activation
|
||||
</span>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-18 PrivateTabIndicator-colorPrimary-19 MuiTabs-indicator"
|
||||
className="PrivateTabIndicator-root-20 PrivateTabIndicator-colorPrimary-21 MuiTabs-indicator"
|
||||
style={Object {}}
|
||||
/>
|
||||
</button>
|
||||
|
@ -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' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
22
frontend/src/component/history/EventHistory/EventHistory.tsx
Normal file
22
frontend/src/component/history/EventHistory/EventHistory.tsx
Normal file
@ -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 <EventLog history={history} title="Recent changes" />;
|
||||
};
|
||||
|
||||
export default EventHistory;
|
16
frontend/src/component/history/EventHistory/index.js
Normal file
16
frontend/src/component/history/EventHistory/index.js
Normal file
@ -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;
|
@ -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 (
|
||||
<>
|
||||
<dt className={styles.eventLogHeader}>Name: </dt>
|
||||
<dd>{name}</dd>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<dl>
|
||||
<dt className={styles.eventLogHeader}>Changed at:</dt>
|
||||
<dd>{timeFormatted}</dd>
|
||||
<dt className={styles.eventLogHeader}>Changed by: </dt>
|
||||
<dd title={entry.createdBy}>{entry.createdBy}</dd>
|
||||
<dt className={styles.eventLogHeader}>Type: </dt>
|
||||
<dd>{entry.type}</dd>
|
||||
{getName(entry.data.name)}
|
||||
</dl>
|
||||
<strong>Change</strong>
|
||||
<EventDiff entry={entry} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventCard;
|
@ -0,0 +1,7 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
eventLogHeader: {
|
||||
minWidth: '110px',
|
||||
},
|
||||
});
|
@ -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 = (
|
||||
<div>
|
||||
<div className={KLASSES.D}>
|
||||
- {key}: {JSON.stringify(diff.lhs)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (diff.rhs !== undefined) {
|
||||
change = (
|
||||
<div>
|
||||
<div className={KLASSES.N}>
|
||||
+ {key}: {JSON.stringify(diff.rhs)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (
|
||||
<div>
|
||||
<div className={KLASSES.D}>
|
||||
- {key}: {JSON.stringify(diff.lhs)}
|
||||
</div>
|
||||
<div className={KLASSES.N}>
|
||||
+ {key}: {JSON.stringify(diff.rhs)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const spadenClass = KLASSES[diff.kind];
|
||||
const prefix = DIFF_PREFIXES[diff.kind];
|
||||
|
||||
change = (
|
||||
<div className={spadenClass}>
|
||||
{prefix} {key}: {JSON.stringify(diff.rhs || diff.item)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div key={idx}>{change}</div>;
|
||||
};
|
||||
|
||||
let changes;
|
||||
|
||||
if (entry.diffs) {
|
||||
changes = entry.diffs.map(buildDiff);
|
||||
} else {
|
||||
// Just show the data if there is no diff yet.
|
||||
changes = (
|
||||
<div className={KLASSES.N}>
|
||||
{JSON.stringify(entry.data, null, 2)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<pre style={{ overflowX: 'auto', overflowY: 'hidden' }}>
|
||||
<code className="smalltext man">
|
||||
{changes.length === 0 ? '(no changes)' : changes}
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
EventDiff.propTypes = {
|
||||
entry: PropTypes.object,
|
||||
};
|
||||
|
||||
export default EventDiff;
|
@ -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,
|
||||
},
|
||||
}));
|
@ -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 (
|
||||
<div className={styles.historyItem}>
|
||||
<div>
|
||||
<code className="JSON smalltext man">{prettyPrinted}</code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
EventJson.propTypes = {
|
||||
entry: PropTypes.object,
|
||||
};
|
||||
|
||||
export default EventJson;
|
@ -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,
|
||||
},
|
||||
},
|
||||
}));
|
93
frontend/src/component/history/EventLog/EventLog.jsx
Normal file
93
frontend/src/component/history/EventLog/EventLog.jsx
Normal file
@ -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 => (
|
||||
<div key={entry.id} className={styles.eventEntry}>
|
||||
<EventCard
|
||||
entry={entry}
|
||||
timeFormatted={formatFulldateTime(entry.createdAt)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (showData) {
|
||||
entries = history.map(entry => (
|
||||
<EventJson key={`log${entry.id}`} entry={entry} />
|
||||
));
|
||||
} else {
|
||||
entries = history.map(renderListItemCards);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
disablePadding={displayInline}
|
||||
disableBorder={displayInline}
|
||||
headerContent={
|
||||
<HeaderTitle
|
||||
title={title}
|
||||
actions={
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={showData}
|
||||
onChange={toggleShowDiff}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Full events"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.history}>
|
||||
<List>{entries}</List>
|
||||
</div>
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
EventLog.propTypes = {
|
||||
updateSettings: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
settings: PropTypes.object,
|
||||
displayInline: PropTypes.bool,
|
||||
location: PropTypes.object,
|
||||
hideName: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default EventLog;
|
40
frontend/src/component/history/EventLog/EventLog.styles.js
Normal file
40
frontend/src/component/history/EventLog/EventLog.styles.js
Normal file
@ -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',
|
||||
},
|
||||
},
|
||||
}));
|
@ -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;
|
@ -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 <span>fetching..</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EventLog history={history} hideName title="Change log" displayInline />
|
||||
);
|
||||
};
|
||||
|
||||
FeatureEventHistory.propTypes = {
|
||||
toggleName: PropTypes.string.isRequired,
|
||||
history: PropTypes.array,
|
||||
fetchHistoryForToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FeatureEventHistory;
|
@ -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;
|
@ -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 (
|
||||
<Card className={commonStyles.fullwidth}>
|
||||
<HistoryList history={history} title="Recent changes" />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default History;
|
@ -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;
|
@ -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 = (
|
||||
<div>
|
||||
<div className={KLASSES.D}>
|
||||
- {key}: {JSON.stringify(diff.lhs)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (diff.rhs !== undefined) {
|
||||
change = (
|
||||
<div>
|
||||
<div className={KLASSES.N}>
|
||||
+ {key}: {JSON.stringify(diff.rhs)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (
|
||||
<div>
|
||||
<div className={KLASSES.D}>
|
||||
- {key}: {JSON.stringify(diff.lhs)}
|
||||
</div>
|
||||
<div className={KLASSES.N}>
|
||||
+ {key}: {JSON.stringify(diff.rhs)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const spadenClass = KLASSES[diff.kind];
|
||||
const prefix = DIFF_PREFIXES[diff.kind];
|
||||
|
||||
change = (
|
||||
<div className={spadenClass}>
|
||||
{prefix} {key}: {JSON.stringify(diff.rhs || diff.item)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div key={idx}>{change}</div>;
|
||||
}
|
||||
|
||||
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 = <div className={KLASSES.N}>{JSON.stringify(entry.data, null, 2)}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<pre style={{ overflowX: 'auto', overflowY: 'hidden' }}>
|
||||
<code className="smalltext man">{changes.length === 0 ? '(no changes)' : changes}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default HistoryItem;
|
@ -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 (
|
||||
<div className={style['history-item']}>
|
||||
<div>
|
||||
<code className="JSON smalltext man">{prettyPrinted}</code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default HistoryItem;
|
@ -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 (
|
||||
<React.Fragment>
|
||||
<dt>Name: </dt>
|
||||
<dd>{name}</dd>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const HistoryMeta = ({ entry, timeFormatted }) => (
|
||||
<div>
|
||||
<dl>
|
||||
<dt>Changed at:</dt>
|
||||
<dd>{timeFormatted}</dd>
|
||||
<dt>Changed by: </dt>
|
||||
<dd title={entry.createdBy}>{entry.createdBy}</dd>
|
||||
<dt>Type: </dt>
|
||||
<dd>{entry.type}</dd>
|
||||
{getName(entry.data.name)}
|
||||
</dl>
|
||||
<strong>Change</strong>
|
||||
<HistoryItemDiff entry={entry} />
|
||||
</div>
|
||||
);
|
||||
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 => (
|
||||
<div key={entry.id} className={styles.eventEntry}>
|
||||
<HistoryMeta entry={entry} timeFormatted={this.formatFulldateTime(entry.createdAt)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (showData) {
|
||||
entries = history.map(entry => <HistoryItemJson key={`log${entry.id}`} entry={entry} />);
|
||||
} else {
|
||||
entries = history.map(renderListItemCards);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
headerContent={
|
||||
<HeaderTitle
|
||||
title={this.props.title}
|
||||
actions={
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={showData}
|
||||
onChange={this.toggleShowDiff.bind(this)}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Full events"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.history}>
|
||||
<List>{entries}</List>
|
||||
</div>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default HistoryList;
|
@ -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 <span>fetching..</span>;
|
||||
}
|
||||
const { history } = this.props;
|
||||
return <HistoryList history={history} hideName title="Change log" />;
|
||||
}
|
||||
}
|
||||
|
||||
export default HistoryListToggle;
|
@ -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;
|
||||
}
|
16
frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx
Normal file
16
frontend/src/component/layout/LayoutPicker/LayoutPicker.jsx
Normal file
@ -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 (
|
||||
<ConditionallyRender
|
||||
condition={isLoginPage}
|
||||
show={children}
|
||||
elseShow={<MainLayout location={location}>{children}</MainLayout>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutPicker;
|
@ -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 }) => {
|
||||
<Grid container className={muiStyles.container}>
|
||||
<div className={classnames(styles.contentWrapper)}>
|
||||
<Grid item className={styles.content} xs={12} sm={12}>
|
||||
<div className={styles.contentContainer}>{children}</div>
|
||||
<div className={styles.contentContainer}>
|
||||
{children}
|
||||
</div>
|
||||
<ErrorContainer />
|
||||
</Grid>
|
||||
</div>
|
||||
<div className={muiStyles.footer}>
|
||||
<Container>
|
||||
<Footer />
|
||||
</Container>
|
||||
</div>
|
||||
<Footer />
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Layout.propTypes = {
|
||||
MainLayout.propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
export default MainLayout;
|
@ -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 = () => (
|
||||
<React.Fragment>
|
||||
<footer>
|
||||
export const Footer = () => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<Grid container>
|
||||
<Grid item xs={3}>
|
||||
<section title="Menu">
|
||||
<h4>Menu</h4>
|
||||
<List className={styles.list}>
|
||||
<ConditionallyRender
|
||||
condition={routes && routes.length > 0}
|
||||
show={routes.map(route => (
|
||||
<ListItem key={`listitem_${route.path}`} className={styles.listItem}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<NavLink key={route.path} to={route.path} className={styles.link}>
|
||||
{route.title}
|
||||
</NavLink>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
/>
|
||||
<ListItem key="github_link" className={styles.listItem}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<a href="https://github.com/Unleash/unleash/" target="_blank" rel="noreferrer">
|
||||
GitHub
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</section>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<section title="Client SDKs">
|
||||
<h4>Client SDKs</h4>
|
||||
<List className={styles.list}>
|
||||
<ListItem>
|
||||
<ListItem className={styles.listItem}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<a
|
||||
@ -58,7 +28,7 @@ export const Footer = () => (
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItem className={styles.listItem}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<a
|
||||
@ -69,17 +39,20 @@ export const Footer = () => (
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</ListItem>{' '}
|
||||
<ListItem>
|
||||
</ListItem>
|
||||
<ListItem className={styles.listItem}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<a href="https://github.com/Unleash/unleash-client-go" className={styles.link}>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-go"
|
||||
className={styles.link}
|
||||
>
|
||||
Go
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</ListItem>{' '}
|
||||
<ListItem>
|
||||
<ListItem className={styles.listItem}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<a
|
||||
@ -91,7 +64,7 @@ export const Footer = () => (
|
||||
}
|
||||
/>
|
||||
</ListItem>{' '}
|
||||
<ListItem>
|
||||
<ListItem className={styles.listItem}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<a
|
||||
@ -103,7 +76,7 @@ export const Footer = () => (
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItem className={styles.listItem}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<a
|
||||
@ -115,10 +88,13 @@ export const Footer = () => (
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItem className={styles.listItem}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<a href="https://unleash.github.io/docs/client_sdk" className={styles.link}>
|
||||
<a
|
||||
href="https://unleash.github.io/docs/client_sdk"
|
||||
className={styles.link}
|
||||
>
|
||||
All client SDKs
|
||||
</a>
|
||||
}
|
||||
@ -132,7 +108,7 @@ export const Footer = () => (
|
||||
</Grid>
|
||||
</Grid>
|
||||
</footer>
|
||||
</React.Fragment>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
@ -10,5 +10,12 @@
|
||||
|
||||
.listItem a {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: #fff;
|
||||
padding: 2rem 4rem;
|
||||
color: #000;
|
||||
width: 100%;
|
||||
}
|
||||
|
21
frontend/src/component/menu/Footer/Footer.styles.js
Normal file
21
frontend/src/component/menu/Footer/Footer.styles.js
Normal file
@ -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,
|
||||
},
|
||||
},
|
||||
}));
|
@ -1,5 +1,341 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should render DrawerMenu 1`] = `<footer />`;
|
||||
exports[`should render DrawerMenu 1`] = `
|
||||
<footer
|
||||
className="makeStyles-footer-1"
|
||||
>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-container"
|
||||
>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-3"
|
||||
>
|
||||
<section
|
||||
title="Client SDKs"
|
||||
>
|
||||
<h4>
|
||||
Client SDKs
|
||||
</h4>
|
||||
<ul
|
||||
className="MuiList-root makeStyles-list-2 MuiList-padding"
|
||||
>
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-node"
|
||||
>
|
||||
Node.js
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-java"
|
||||
>
|
||||
Java
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-go"
|
||||
>
|
||||
Go
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-ruby"
|
||||
>
|
||||
Ruby
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-python"
|
||||
>
|
||||
Python
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-core"
|
||||
>
|
||||
.Net Core
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://unleash.github.io/docs/client_sdk"
|
||||
>
|
||||
All client SDKs
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
||||
>
|
||||
<section
|
||||
title="API details"
|
||||
>
|
||||
<h4>
|
||||
undefined undefined
|
||||
</h4>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<small />
|
||||
<br />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
`;
|
||||
|
||||
exports[`should render DrawerMenu with "features" selected 1`] = `<footer />`;
|
||||
exports[`should render DrawerMenu with "features" selected 1`] = `
|
||||
<footer
|
||||
className="makeStyles-footer-1"
|
||||
>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-container"
|
||||
>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-3"
|
||||
>
|
||||
<section
|
||||
title="Client SDKs"
|
||||
>
|
||||
<h4>
|
||||
Client SDKs
|
||||
</h4>
|
||||
<ul
|
||||
className="MuiList-root makeStyles-list-2 MuiList-padding"
|
||||
>
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-node"
|
||||
>
|
||||
Node.js
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-java"
|
||||
>
|
||||
Java
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-go"
|
||||
>
|
||||
Go
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-ruby"
|
||||
>
|
||||
Ruby
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-python"
|
||||
>
|
||||
Python
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash-client-core"
|
||||
>
|
||||
.Net Core
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="MuiListItem-root makeStyles-listItem-3 MuiListItem-gutters"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
<a
|
||||
href="https://unleash.github.io/docs/client_sdk"
|
||||
>
|
||||
All client SDKs
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div
|
||||
className="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
|
||||
>
|
||||
<section
|
||||
title="API details"
|
||||
>
|
||||
<h4>
|
||||
undefined undefined
|
||||
</h4>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<small />
|
||||
<br />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
`;
|
||||
|
@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
@ -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(
|
||||
<MemoryRouter>
|
||||
<Footer />
|
||||
</MemoryRouter>
|
||||
<Provider store={createStore(mockReducer, mockStore)}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<MemoryRouter>
|
||||
<Footer />
|
||||
</MemoryRouter>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
@ -18,9 +36,13 @@ test('should render DrawerMenu', () => {
|
||||
|
||||
test('should render DrawerMenu with "features" selected', () => {
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter initialEntries={['/features']}>
|
||||
<Footer />
|
||||
</MemoryRouter>
|
||||
<Provider store={createStore(mockReducer, mockStore)}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<MemoryRouter initialEntries={['/features']}>
|
||||
<Footer />
|
||||
</MemoryRouter>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
|
61
frontend/src/component/user/Login/Login.jsx
Normal file
61
frontend/src/component/user/Login/Login.jsx
Normal file
@ -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 (
|
||||
<div className={styles.loginContainer}>
|
||||
<div className={classnames(styles.container)}>
|
||||
<div
|
||||
className={classnames(
|
||||
styles.contentContainer,
|
||||
styles.gradient
|
||||
)}
|
||||
>
|
||||
<h1 className={styles.title}>
|
||||
<UnleashLogo className={styles.logo} /> Unleash
|
||||
</h1>
|
||||
<Typography variant="body1" className={styles.subTitle}>
|
||||
Committed to creating new ways of developing
|
||||
</Typography>
|
||||
<ConditionallyRender
|
||||
condition={smallScreen}
|
||||
show={
|
||||
<div className={styles.imageContainer}>
|
||||
<SwitchesSVG />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.contentContainer}>
|
||||
<h2 className={styles.title}>Login</h2>
|
||||
<div className={styles.loginFormContainer}>
|
||||
<AuthenticationContainer history={history} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
48
frontend/src/component/user/Login/Login.styles.js
Normal file
48
frontend/src/component/user/Login/Login.styles.js
Normal file
@ -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',
|
||||
},
|
||||
}));
|
14
frontend/src/component/user/Login/index.js
Normal file
14
frontend/src/component/user/Login/index.js
Normal file
@ -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);
|
@ -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 (
|
||||
<form onSubmit={handleSubmit} action={authDetails.path} className={styles.loginContainer}>
|
||||
<Typography variant="subtitle1">{authDetails.message}</Typography>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
action={authDetails.path}
|
||||
className={styles.loginContainer}
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
{authDetails.message}
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" className={styles.apiError}>
|
||||
{apiError}
|
||||
</Typography>
|
||||
<div className={classnames(styles.contentContainer, commonStyles.contentSpacingY)}>
|
||||
<div
|
||||
className={classnames(
|
||||
styles.contentContainer,
|
||||
commonStyles.contentSpacingY
|
||||
)}
|
||||
>
|
||||
<TextField
|
||||
label="Username or email"
|
||||
name="username"
|
||||
@ -100,7 +117,12 @@ const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => {
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<Button variant="contained" color="primary" type="submit">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
style={{ maxWidth: '150px' }}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</div>
|
||||
@ -121,8 +143,9 @@ const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => {
|
||||
condition={showFields}
|
||||
show={renderLoginForm()}
|
||||
elseShow={
|
||||
<IconButton> onClick={onShowOptions}>
|
||||
Show more options
|
||||
<IconButton>
|
||||
{' '}
|
||||
onClick={onShowOptions} Show more options
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
|
@ -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 }) =>
|
||||
<div className={styles.container}>
|
||||
<p>{authDetails.message}</p>
|
||||
<p>
|
||||
This instance of Unleash is not set up with a secure authentication provider. You can read more
|
||||
about{' '}
|
||||
<a href="https://github.com/Unleash/unleash/blob/master/docs/securing-unleash.md" target="_blank" rel="noreferrer">
|
||||
This instance of Unleash is not set up with a secure
|
||||
authentication provider. You can read more about{' '}
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash/blob/master/docs/securing-unleash.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
securing Unleash on GitHub
|
||||
</a>
|
||||
</p>
|
||||
@ -47,7 +56,13 @@ const SimpleAuth = ({ insecureLogin, loadInitialData, history, authDetails }) =>
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<Button type="submit" variant="contained" color="primary" data-test="login-submit">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
data-test="login-submit"
|
||||
className={styles.button}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -1,3 +1,7 @@
|
||||
.container > * {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
margin: 0.6rem 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
@ -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 = (
|
||||
<AuthenticationPasswordComponent
|
||||
<PasswordAuth
|
||||
passwordLogin={this.props.passwordLogin}
|
||||
authDetails={authDetails}
|
||||
loadInitialData={this.props.loadInitialData}
|
||||
@ -43,27 +40,11 @@ class AuthComponent extends React.Component {
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = <AuthenticationCustomComponent authDetails={authDetails} />;
|
||||
content = (
|
||||
<AuthenticationCustomComponent authDetails={authDetails} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={this.props.user.showDialog} style={customStyles}>
|
||||
<DialogTitle
|
||||
id="simple-dialog-title"
|
||||
style={{
|
||||
background: 'rgb(96, 125, 139)',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Icon style={{ marginRight: '8px' }}>person</Icon> Login
|
||||
</span>
|
||||
</DialogTitle>
|
||||
|
||||
<div style={{ padding: '1rem' }}>{content}</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
return <div>{content}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 (<Card shadow={0} className={commonStyles.fullwidth}>
|
||||
return (
|
||||
<Card shadow={0} className={commonStyles.fullwidth}>
|
||||
<CardHeader>Logged out</CardHeader>
|
||||
<CardContent>
|
||||
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.{' '}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
LogoutComponent.propTypes = {
|
||||
logoutUser: PropTypes.func.isRequired
|
||||
}
|
||||
logoutUser: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default LogoutComponent;
|
||||
|
||||
|
@ -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 (
|
||||
<div className={styles.showUserSettings}>
|
||||
<DropdownMenu
|
||||
className={styles.dropdown}
|
||||
startIcon={<Icon component={'img'} alt={locale} src={imageLocale} className={styles.labelFlag} />}
|
||||
startIcon={
|
||||
<Icon
|
||||
component={'img'}
|
||||
alt={locale}
|
||||
src={imageLocale}
|
||||
className={styles.labelFlag}
|
||||
/>
|
||||
}
|
||||
renderOptions={() =>
|
||||
this.possibleLocales.map(i => (
|
||||
<MenuItem key={i.value} onClick={() => this.setLocale(i)}>
|
||||
<MenuItem
|
||||
key={i.value}
|
||||
onClick={() => this.setLocale(i)}
|
||||
>
|
||||
<div className={styles.showLocale}>
|
||||
<img src={`${i.image}.png`} title={i.value} alt={i.value} />
|
||||
<Typography variant="p">{i.value}</Typography>
|
||||
<img
|
||||
src={`${i.image}.png`}
|
||||
title={i.value}
|
||||
alt={i.value}
|
||||
/>
|
||||
<Typography variant="p">
|
||||
{i.value}
|
||||
</Typography>
|
||||
</div>
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
label="Locale"
|
||||
/>
|
||||
<Avatar alt="user image" src={imageUrl} className={styles.avatar} />
|
||||
<Avatar
|
||||
alt="user image"
|
||||
src={imageUrl}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
11
frontend/src/icons/switches.svg
Normal file
11
frontend/src/icons/switches.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="197" height="310" viewBox="0 0 197 310" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M126.669 92.7899C134.256 92.7899 140.408 86.642 140.408 79.0581C140.408 71.4742 134.256 65.3263 126.669 65.3263C119.081 65.3263 112.93 71.4742 112.93 79.0581C112.93 86.642 119.081 92.7899 126.669 92.7899Z" fill="white"/>
|
||||
<path d="M129.417 99.6559H67.5913C62.1256 99.6559 56.8838 97.4858 53.019 93.623C49.1541 89.7601 46.9829 84.521 46.9829 79.0582C46.9829 73.5953 49.1541 68.3562 53.019 64.4934C56.8838 60.6306 62.1256 58.4604 67.5913 58.4604H129.417C134.882 58.4604 140.124 60.6306 143.989 64.4934C147.854 68.3562 150.025 73.5953 150.025 79.0582C150.025 84.521 147.854 89.7601 143.989 93.623C140.124 97.4858 134.882 99.6559 129.417 99.6559ZM67.5913 61.2068C62.8544 61.2068 58.3114 63.0876 54.9619 66.4354C51.6124 69.7831 49.7307 74.3237 49.7307 79.0582C49.7307 83.7926 51.6124 88.3332 54.9619 91.681C58.3114 95.0288 62.8544 96.9095 67.5913 96.9095H129.417C134.153 96.9095 138.696 95.0288 142.046 91.681C145.395 88.3332 147.277 83.7926 147.277 79.0582C147.277 74.3237 145.395 69.7831 142.046 66.4354C138.696 63.0876 134.153 61.2068 129.417 61.2068H67.5913Z" fill="white"/>
|
||||
<path d="M69.818 168.383C77.4058 168.383 83.5569 162.235 83.5569 154.651C83.5569 147.067 77.4058 140.919 69.818 140.919C62.2302 140.919 56.0791 147.067 56.0791 154.651C56.0791 162.235 62.2302 168.383 69.818 168.383Z" fill="white"/>
|
||||
<path d="M129.417 175.248H67.5913C62.1256 175.248 56.8838 173.078 53.019 169.216C49.1541 165.353 46.9829 160.114 46.9829 154.651C46.9829 149.188 49.1541 143.949 53.019 140.086C56.8838 136.223 62.1256 134.053 67.5913 134.053H129.417C134.882 134.053 140.124 136.223 143.989 140.086C147.854 143.949 150.025 149.188 150.025 154.651C150.025 160.114 147.854 165.353 143.989 169.216C140.124 173.078 134.882 175.248 129.417 175.248ZM67.5913 136.799C62.8544 136.799 58.3115 138.68 54.962 142.028C51.6124 145.376 49.7307 149.916 49.7307 154.651C49.7307 159.385 51.6124 163.926 54.962 167.274C58.3115 170.621 62.8544 172.502 67.5913 172.502H129.417C134.153 172.502 138.696 170.621 142.046 167.274C145.395 163.926 147.277 159.385 147.277 154.651C147.277 149.916 145.395 145.376 142.046 142.028C138.696 138.68 134.153 136.799 129.417 136.799H67.5913Z" fill="white"/>
|
||||
<path d="M69.818 243.975C77.4058 243.975 83.5569 237.827 83.5569 230.243C83.5569 222.659 77.4058 216.512 69.818 216.512C62.2302 216.512 56.0791 222.659 56.0791 230.243C56.0791 237.827 62.2302 243.975 69.818 243.975Z" fill="white"/>
|
||||
<path d="M129.417 250.841H67.5913C62.1256 250.841 56.8838 248.671 53.019 244.808C49.1541 240.945 46.9829 235.706 46.9829 230.243C46.9829 224.78 49.1541 219.541 53.019 215.679C56.8838 211.816 62.1256 209.646 67.5913 209.646H129.417C134.882 209.646 140.124 211.816 143.989 215.679C147.854 219.541 150.025 224.78 150.025 230.243C150.025 235.706 147.854 240.945 143.989 244.808C140.124 248.671 134.882 250.841 129.417 250.841ZM67.5913 212.392C62.8544 212.392 58.3114 214.273 54.9619 217.621C51.6124 220.968 49.7307 225.509 49.7307 230.243C49.7307 234.978 51.6124 239.518 54.9619 242.866C58.3114 246.214 62.8544 248.095 67.5913 248.095H129.417C131.762 248.095 134.085 247.633 136.251 246.736C138.418 245.839 140.387 244.524 142.046 242.866C143.704 241.209 145.02 239.241 145.918 237.075C146.815 234.909 147.277 232.588 147.277 230.243C147.277 227.899 146.815 225.578 145.918 223.412C145.02 221.246 143.704 219.278 142.046 217.621C140.387 215.963 138.418 214.648 136.251 213.751C134.085 212.854 131.762 212.392 129.417 212.392H67.5913Z" fill="white"/>
|
||||
<path d="M193.97 309.126H3.03846C2.23497 309.125 1.46466 308.806 0.896511 308.238C0.328359 307.67 0.00875734 306.9 0.0078125 306.097V3.20437C0.00875734 2.4013 0.328359 1.63139 0.896511 1.06353C1.46466 0.495674 2.23497 0.176237 3.03846 0.175293H193.97C194.773 0.176237 195.543 0.495674 196.111 1.06353C196.68 1.63139 196.999 2.4013 197 3.20437V306.097C196.999 306.9 196.68 307.67 196.111 308.238C195.543 308.806 194.773 309.125 193.97 309.126ZM3.03846 1.38692C2.55635 1.38745 2.09415 1.5791 1.75325 1.91982C1.41235 2.26054 1.2206 2.72251 1.22007 3.20437V306.097C1.2206 306.579 1.41235 307.041 1.75325 307.382C2.09415 307.722 2.55635 307.914 3.03846 307.915H193.97C194.452 307.914 194.914 307.722 195.255 307.382C195.596 307.041 195.787 306.579 195.788 306.097V3.20437C195.787 2.72251 195.596 2.26054 195.255 1.91982C194.914 1.5791 194.452 1.38745 193.97 1.38692H3.03846Z" fill="white"/>
|
||||
<path d="M193.969 309H3.03077C2.22725 308.999 1.45691 308.68 0.888733 308.112C0.320559 307.544 0.000944877 306.774 0 305.97V3.02956C0.000944877 2.22636 0.320559 1.45633 0.888733 0.888379C1.45691 0.320432 2.22725 0.0009445 3.03077 0H193.969C194.773 0.0009445 195.543 0.320432 196.111 0.888379C196.679 1.45633 196.999 2.22636 197 3.02956V305.97C196.999 306.774 196.679 307.544 196.111 308.112C195.543 308.68 194.773 308.999 193.969 309ZM3.03077 1.21182C2.54864 1.21235 2.08642 1.40403 1.7455 1.74481C1.40459 2.08558 1.21283 2.54763 1.21231 3.02956V305.97C1.21283 306.452 1.40459 306.914 1.7455 307.255C2.08642 307.596 2.54864 307.788 3.03077 307.788H193.969C194.451 307.788 194.914 307.596 195.255 307.255C195.595 306.914 195.787 306.452 195.788 305.97V3.02956C195.787 2.54763 195.595 2.08558 195.255 1.74481C194.914 1.40403 194.451 1.21235 193.969 1.21182H3.03077Z" fill="white"/>
|
||||
<path d="M193.969 309H3.03077C2.22725 308.999 1.45691 308.68 0.888733 308.112C0.320559 307.544 0.000944877 306.774 0 305.97V3.02956C0.000944877 2.22636 0.320559 1.45633 0.888733 0.888379C1.45691 0.320432 2.22725 0.0009445 3.03077 0H193.969C194.773 0.0009445 195.543 0.320432 196.111 0.888379C196.679 1.45633 196.999 2.22636 197 3.02956V305.97C196.999 306.774 196.679 307.544 196.111 308.112C195.543 308.68 194.773 308.999 193.969 309ZM3.03077 1.21182C2.54864 1.21235 2.08642 1.40403 1.7455 1.74481C1.40459 2.08558 1.21283 2.54763 1.21231 3.02956V305.97C1.21283 306.452 1.40459 306.914 1.7455 307.255C2.08642 307.596 2.54864 307.788 3.03077 307.788H193.969C194.451 307.788 194.914 307.596 195.255 307.255C195.595 306.914 195.787 306.452 195.788 305.97V3.02956C195.787 2.54763 195.595 2.08558 195.255 1.74481C194.914 1.40403 194.451 1.21235 193.969 1.21182H3.03077Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
4
frontend/src/icons/unleash-logo-inverted.svg
Normal file
4
frontend/src/icons/unleash-logo-inverted.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="177" height="100" viewBox="0 0 177 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="177" height="100" rx="42.5" fill="white"/>
|
||||
<rect x="92" y="24" width="62" height="54" rx="15" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 229 B |
@ -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();
|
||||
|
||||
|
16
frontend/src/interfaces/route.ts
Normal file
16
frontend/src/interfaces/route.ts
Normal file
@ -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;
|
24
frontend/src/interfaces/user.ts
Normal file
24
frontend/src/interfaces/user.ts
Normal file
@ -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;
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import HistoryComponent from '../../component/history/history-container';
|
||||
import HistoryComponent from '../../component/history/EventHistory';
|
||||
|
||||
const render = () => <HistoryComponent />;
|
||||
|
||||
|
@ -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 } }) => <HistoryListToggle toggleName={params.toggleName} />;
|
||||
const render = ({ match: { params } }) => (
|
||||
<HistoryListToggle toggleName={params.toggleName} />
|
||||
);
|
||||
|
||||
render.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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: {
|
||||
|
Loading…
Reference in New Issue
Block a user