From b3436b5ae6d7f3a1a7a8796a5f4e119332f7b2a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Fri, 16 Apr 2021 11:31:47 +0200 Subject: [PATCH] fix: make admin pages work for OSS and enterprise (#268) * fix: make admin pages work for OSS and enterprise * fix: more admin tuning * fix: project mgm access --- .../__snapshots__/routes-test.jsx.snap | 80 +++++++++------- .../component/menu/__tests__/routes-test.jsx | 2 +- frontend/src/component/menu/routes.js | 69 +++++++------ .../project/ProjectList/ProjectList.jsx | 20 +++- .../component/project/ProjectList/index.jsx | 3 - .../src/component/project/list-component.jsx | 96 ------------------- frontend/src/page/admin/admin-menu.jsx | 37 +++---- frontend/src/page/admin/api/api-howto.jsx | 25 ----- .../page/admin/api/api-key-list-container.js | 1 + frontend/src/page/admin/api/api-key-list.jsx | 22 ++++- frontend/src/page/admin/api/index.js | 4 +- .../src/page/admin/auth/authentication.jsx | 56 +++++++++++ .../page/admin/auth/google-auth-container.js | 1 + frontend/src/page/admin/auth/google-auth.jsx | 12 ++- frontend/src/page/admin/auth/index.js | 42 ++------ .../page/admin/auth/saml-auth-container.js | 1 + frontend/src/page/admin/auth/saml-auth.jsx | 14 +-- .../page/admin/users/UsersList/UsersList.jsx | 22 ++--- frontend/src/page/admin/users/index.js | 4 +- 19 files changed, 236 insertions(+), 275 deletions(-) delete mode 100644 frontend/src/component/project/list-component.jsx delete mode 100644 frontend/src/page/admin/api/api-howto.jsx create mode 100644 frontend/src/page/admin/auth/authentication.jsx diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap index bcc21a42e7..5cf0b5802c 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap @@ -93,6 +93,15 @@ Array [ "title": "Sign out", "type": "protected", }, + Object { + "component": [Function], + "hidden": false, + "icon": "album", + "layout": "main", + "path": "/admin", + "title": "Admin", + "type": "protected", + }, ] `; @@ -260,39 +269,6 @@ Array [ "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", @@ -389,5 +365,43 @@ Array [ "title": "Log in", "type": "unprotected", }, + 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": Object { + "$$typeof": Symbol(react.memo), + "WrappedComponent": [Function], + "compare": null, + "type": [Function], + }, + "layout": "main", + "parent": "/admin", + "path": "/admin/auth", + "title": "Authentication", + "type": "protected", + }, + Object { + "component": [Function], + "hidden": false, + "icon": "album", + "layout": "main", + "path": "/admin", + "title": "Admin", + "type": "protected", + }, ] `; diff --git a/frontend/src/component/menu/__tests__/routes-test.jsx b/frontend/src/component/menu/__tests__/routes-test.jsx index ef62a7490e..44effad700 100644 --- a/frontend/src/component/menu/__tests__/routes-test.jsx +++ b/frontend/src/component/menu/__tests__/routes-test.jsx @@ -6,7 +6,7 @@ test('returns all defined routes', () => { }); test('returns all baseRoutes', () => { - expect(baseRoutes.length).toEqual(11); + expect(baseRoutes.length).toEqual(12); expect(baseRoutes).toMatchSnapshot(); }); diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js index 06c9937b82..9bf18d5928 100644 --- a/frontend/src/component/menu/routes.js +++ b/frontend/src/component/menu/routes.js @@ -213,41 +213,6 @@ export const routes = [ layout: 'main', }, - // Admin - { - path: '/admin/api', - 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', - title: 'Admin', - icon: 'album', - component: Admin, - hidden: true, - type: 'protected', - layout: 'main', - }, - { path: '/tag-types/create', parent: '/tag-types', @@ -342,6 +307,40 @@ export const routes = [ hidden: true, layout: 'standalone', }, + // Admin + { + path: '/admin/api', + 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', + title: 'Admin', + icon: 'album', + component: Admin, + hidden: false, + type: 'protected', + layout: 'main', + }, ]; export const getRoute = path => routes.find(route => route.path === path); diff --git a/frontend/src/component/project/ProjectList/ProjectList.jsx b/frontend/src/component/project/ProjectList/ProjectList.jsx index 40a53dd9a9..19f355d4c2 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.jsx +++ b/frontend/src/component/project/ProjectList/ProjectList.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; import HeaderTitle from '../../common/HeaderTitle'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; -import { CREATE_PROJECT, DELETE_PROJECT } from '../../../permissions'; +import { CREATE_PROJECT, DELETE_PROJECT, UPDATE_PROJECT } from '../../../permissions'; import { Icon, IconButton, List, ListItem, ListItemAvatar, ListItemText, Tooltip } from '@material-ui/core'; import { Link } from 'react-router-dom'; import ConfirmDialogue from '../../common/Dialogue'; @@ -36,6 +36,16 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history, hasPermi ); + const mgmAccessButton = project => ( + + + + supervised_user_circle + + + + ); + const deleteProjectButton = project => ( folder_open + )); return ( - }> + }> 0} @@ -93,8 +107,6 @@ ProjectList.propTypes = { removeProject: PropTypes.func.isRequired, history: PropTypes.object.isRequired, hasPermission: PropTypes.func.isRequired, - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, }; export default ProjectList; diff --git a/frontend/src/component/project/ProjectList/index.jsx b/frontend/src/component/project/ProjectList/index.jsx index c750d7bf18..f562a7764b 100644 --- a/frontend/src/component/project/ProjectList/index.jsx +++ b/frontend/src/component/project/ProjectList/index.jsx @@ -1,16 +1,13 @@ import { connect } from 'react-redux'; import { fetchProjects, removeProject } from '../../../store/project/actions'; import { hasPermission } from '../../../permissions'; -import { RBAC } from '../../common/flags'; import ProjectList from './ProjectList'; const mapStateToProps = state => { const projects = state.projects.toJS(); - const rbacEnabled = !!state.uiConfig.toJS().flags[RBAC]; return { projects, - rbacEnabled, hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; diff --git a/frontend/src/component/project/list-component.jsx b/frontend/src/component/project/list-component.jsx deleted file mode 100644 index d60547a1b7..0000000000 --- a/frontend/src/component/project/list-component.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; - -import { List, ListItem, IconButton, Icon, Paper, ListItemAvatar, ListItemText, Tooltip } from '@material-ui/core'; -import { HeaderTitle, styles as commonStyles } from '../common'; -import { CREATE_PROJECT, DELETE_PROJECT } from '../../permissions'; -import ConditionallyRender from '../common/conditionally-render'; - -class ProjectListComponent extends Component { - static propTypes = { - projects: PropTypes.array.isRequired, - fetchProjects: PropTypes.func.isRequired, - removeProject: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, - rbacEnabled: PropTypes.bool.isRequired, - }; - - componentDidMount() { - this.props.fetchProjects(); - } - - removeProject = (project, evt) => { - evt.preventDefault(); - this.props.removeProject(project); - }; - - projectLink = ({ id, name }) => ( - - {name} - - ); - - deleteProjectButton = project => ( - - - delete - - - ); - - projectList = () => { - const { projects, hasPermission } = this.props; - return projects.map((project, i) => ( - - - folder_open - - - - - )); - }; - - addProjectButton = () => { - const { hasPermission } = this.props; - return ( - - this.props.history.push('/projects/create')} - > - add - - - } - /> - ); - }; - - render() { - const { projects } = this.props; - - return ( - - - - 0} - show={this.projectList()} - elseShow={No projects defined} - /> - - - ); - } -} - -export default ProjectListComponent; diff --git a/frontend/src/page/admin/admin-menu.jsx b/frontend/src/page/admin/admin-menu.jsx index 7ea0eaeb6e..91c73ee67d 100644 --- a/frontend/src/page/admin/admin-menu.jsx +++ b/frontend/src/page/admin/admin-menu.jsx @@ -1,7 +1,6 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; -import { Grid, Icon } from '@material-ui/core'; -import PageContent from '../../component/common/PageContent/PageContent'; +import { Paper, Icon, Tabs, Tab } from '@material-ui/core'; const navLinkStyle = { display: 'flex', @@ -23,30 +22,36 @@ const iconStyle = { marginRight: '5px', }; -function AdminMenu() { +function AdminMenu({history}) { + const { location } = history; + const { pathname } = location; return ( - - - + + + - supervised_user_circle - Users - - - + supervised_user_circle + Users + + } + > + + apps API Access - - + }> + + lock Authentication - - - + }> + + + ); } diff --git a/frontend/src/page/admin/api/api-howto.jsx b/frontend/src/page/admin/api/api-howto.jsx deleted file mode 100644 index 47f59ecab6..0000000000 --- a/frontend/src/page/admin/api/api-howto.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -function ApiHowTo() { - return ( -
-

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

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

-
- ); -} - -export default ApiHowTo; diff --git a/frontend/src/page/admin/api/api-key-list-container.js b/frontend/src/page/admin/api/api-key-list-container.js index 6988d6d963..ec1553ab8a 100644 --- a/frontend/src/page/admin/api/api-key-list-container.js +++ b/frontend/src/page/admin/api/api-key-list-container.js @@ -7,6 +7,7 @@ import { hasPermission } from '../../../permissions'; export default connect( state => ({ location: state.settings.toJS().location || {}, + unleashUrl: state.uiConfig.toJS().unleashUrl, keys: state.apiAdmin.toJS(), hasPermission: permission => hasPermission(state.user.get('profile'), permission), }), diff --git a/frontend/src/page/admin/api/api-key-list.jsx b/frontend/src/page/admin/api/api-key-list.jsx index 888d7439fc..94e7e00c0a 100644 --- a/frontend/src/page/admin/api/api-key-list.jsx +++ b/frontend/src/page/admin/api/api-key-list.jsx @@ -2,14 +2,14 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { Icon, Table, TableHead, TableBody, TableRow, TableCell, IconButton } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; import { formatFullDateTimeWithLocale } from '../../../component/common/util'; import CreateApiKey from './api-key-create'; import Secret from './secret'; -import ApiHowTo from './api-howto'; import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender'; import Dialogue from '../../../component/common/Dialogue/Dialogue'; -function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermission }) { +function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermission, unleashUrl }) { const [showDelete, setShowDelete] = useState(false); const [delKey, setDelKey] = useState(undefined); const deleteKey = async () => { @@ -25,7 +25,22 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermis return (
- + +

+ Read the{' '} + + Getting started guide + {' '} + to learn how to connect to the Unleash API form your application or programmatically. + Please note it can take up to 1 minute before a new API key is activated. +

+
+ API URL:
{unleashUrl}/api/
+
+ +

+ +
@@ -93,6 +108,7 @@ ApiKeyList.propTypes = { removeKey: PropTypes.func.isRequired, addKey: PropTypes.func.isRequired, keys: PropTypes.array.isRequired, + unleashUrl: PropTypes.string, hasPermission: PropTypes.func.isRequired, }; diff --git a/frontend/src/page/admin/api/index.js b/frontend/src/page/admin/api/index.js index 8deb67c81d..4d6ada91cc 100644 --- a/frontend/src/page/admin/api/index.js +++ b/frontend/src/page/admin/api/index.js @@ -5,9 +5,9 @@ import ApiKeyList from './api-key-list-container'; import AdminMenu from '../admin-menu'; import PageContent from '../../../component/common/PageContent/PageContent'; -const render = () => ( +const render = ({history}) => (
- + diff --git a/frontend/src/page/admin/auth/authentication.jsx b/frontend/src/page/admin/auth/authentication.jsx new file mode 100644 index 0000000000..cd2c065949 --- /dev/null +++ b/frontend/src/page/admin/auth/authentication.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import AdminMenu from '../admin-menu'; +import { Alert } from '@material-ui/lab'; +import GoogleAuth from './google-auth-container'; +import SamlAuth from './saml-auth-container'; +import TabNav from '../../../component/common/TabNav/TabNav'; +import PageContent from '../../../component/common/PageContent/PageContent'; +import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender'; + +function AdminAuthPage({ authenticationType, history }) { + const tabs = [ + { + label: 'SAML 2.0', + component: , + }, + { + label: 'Google', + component: , + }, + ]; + + return ( +
+ + + + } + /> + + You are running the open-source version of Unleash. You have to use the Enterprise edition + in order configure Single Sign-on. + } + /> + You have decided to use custom authentication type. You have to use the Enterprise edition + in order configure Single Sign-on from the user interface. + } + /> + +
+ ); +} + +AdminAuthPage.propTypes = { + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + authenticationType: PropTypes.string, +}; + +export default AdminAuthPage; diff --git a/frontend/src/page/admin/auth/google-auth-container.js b/frontend/src/page/admin/auth/google-auth-container.js index 6557d9c059..e5cc107d77 100644 --- a/frontend/src/page/admin/auth/google-auth-container.js +++ b/frontend/src/page/admin/auth/google-auth-container.js @@ -5,6 +5,7 @@ import { hasPermission } from '../../../permissions'; const mapStateToProps = state => ({ config: state.authAdmin.get('google'), + unleashUrl: state.uiConfig.toJS().unleashUrl, hasPermission: permission => hasPermission(state.user.get('profile'), permission), }); diff --git a/frontend/src/page/admin/auth/google-auth.jsx b/frontend/src/page/admin/auth/google-auth.jsx index b960ac73fa..8d5c3770ee 100644 --- a/frontend/src/page/admin/auth/google-auth.jsx +++ b/frontend/src/page/admin/auth/google-auth.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Button, Grid, Switch, TextField, Typography } from '@material-ui/core'; +import { Button, Grid, Switch, TextField } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; import PageContent from '../../../component/common/PageContent/PageContent'; const initialState = { @@ -9,7 +10,7 @@ const initialState = { unleashHostname: location.hostname, }; -function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission }) { +function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission, unleashUrl }) { const [data, setData] = useState(initialState); const [info, setInfo] = useState(); @@ -58,14 +59,14 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission - + Please read the{' '} documentation {' '} to learn how to integrate with Google OAuth 2.0.
- Callback URL: https://[unleash.hostname.com]/auth/google/callback -
+ Callback URL: {unleashUrl}/auth/google/callback +
@@ -189,6 +190,7 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission GoogleAuth.propTypes = { config: PropTypes.object, + unleashUrl: PropTypes.string, getGoogleConfig: PropTypes.func.isRequired, updateGoogleConfig: PropTypes.func.isRequired, hasPermission: PropTypes.func.isRequired, diff --git a/frontend/src/page/admin/auth/index.js b/frontend/src/page/admin/auth/index.js index 056c606455..05a89bf256 100644 --- a/frontend/src/page/admin/auth/index.js +++ b/frontend/src/page/admin/auth/index.js @@ -1,36 +1,12 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import AdminMenu from '../admin-menu'; -import GoogleAuth from './google-auth-container'; -import SamlAuth from './saml-auth-container'; -import TabNav from '../../../component/common/TabNav/TabNav'; -import PageContent from '../../../component/common/PageContent/PageContent'; +import { connect } from 'react-redux'; +import component from './authentication'; +import { hasPermission } from '../../../permissions'; -function AdminAuthPage() { - const tabs = [ - { - label: 'SAML 2.0', - component: , - }, - { - label: 'Google', - component: , - }, - ]; +const mapStateToProps = state => ({ + authenticationType: state.uiConfig.toJS().authenticationType, + hasPermission: permission => hasPermission(state.user.get('profile'), permission), +}); - return ( -
- - - - -
- ); -} +const Container = connect(mapStateToProps, { })(component); -AdminAuthPage.propTypes = { - match: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, -}; - -export default AdminAuthPage; +export default Container; diff --git a/frontend/src/page/admin/auth/saml-auth-container.js b/frontend/src/page/admin/auth/saml-auth-container.js index 4340f8e798..f91ffab053 100644 --- a/frontend/src/page/admin/auth/saml-auth-container.js +++ b/frontend/src/page/admin/auth/saml-auth-container.js @@ -5,6 +5,7 @@ import { hasPermission } from '../../../permissions'; const mapStateToProps = state => ({ config: state.authAdmin.get('saml'), + unleashUrl: state.uiConfig.toJS().unleashUrl, hasPermission: permission => hasPermission(state.user.get('profile'), permission), }); diff --git a/frontend/src/page/admin/auth/saml-auth.jsx b/frontend/src/page/admin/auth/saml-auth.jsx index bdefe138e4..8e819bbcce 100644 --- a/frontend/src/page/admin/auth/saml-auth.jsx +++ b/frontend/src/page/admin/auth/saml-auth.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Button, Grid, Switch, TextField, Typography } from '@material-ui/core'; +import { Button, Grid, Switch, TextField } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; import PageContent from '../../../component/common/PageContent/PageContent'; const initialState = { @@ -9,7 +10,7 @@ const initialState = { unleashHostname: location.hostname, }; -function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) { +function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission, unleashUrl }) { const [data, setData] = useState(initialState); const [info, setInfo] = useState(); @@ -26,7 +27,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) { }, [config]); if (!hasPermission('ADMIN')) { - return You need admin privileges to access this section.; + return You need to be a root admin to access this section.; } const updateField = e => { @@ -59,14 +60,14 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) { - + Please read the{' '} documentation {' '} to learn how to integrate with specific SAML 2.0 providers (Okta, Keycloak, etc).
- Callback URL: https://[unleash.hostname.com]/auth/saml/callback -
+ Callback URL: {unleashUrl}/auth/saml/callback +
@@ -184,6 +185,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) { SamlAuth.propTypes = { config: PropTypes.object, + unleash: PropTypes.string, getSamlConfig: PropTypes.func.isRequired, updateSamlConfig: PropTypes.func.isRequired, hasPermission: PropTypes.func.isRequired, diff --git a/frontend/src/page/admin/users/UsersList/UsersList.jsx b/frontend/src/page/admin/users/UsersList/UsersList.jsx index 54c4c82f5d..fe42e72f93 100644 --- a/frontend/src/page/admin/users/UsersList/UsersList.jsx +++ b/frontend/src/page/admin/users/UsersList/UsersList.jsx @@ -1,8 +1,8 @@ /* eslint-disable no-alert */ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core'; -import { formatFullDateTimeWithLocale } from '../../../../component/common/util'; +import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow, Avatar } from '@material-ui/core'; +import { formatDateWithLocale } from '../../../../component/common/util'; import AddUser from '../add-user-component'; import ChangePassword from '../change-password-component'; import UpdateUser from '../update-user-component'; @@ -78,26 +78,26 @@ function UsersList({
- Id + Created - Username Name - Role - {hasPermission('ADMIN') ? 'Action' : ''} + Username + Role + {hasPermission('ADMIN') ? 'Action' : ''} {users.map(item => ( - {item.id} - {formatFullDateTimeWithLocale(item.createdAt, location.locale)} - {item.username || item.email} + + {formatDateWithLocale(item.createdAt, location.locale)} {item.name} - {renderRole(item.rootRole)} + {item.username || item.email} + {renderRole(item.rootRole)} + edit diff --git a/frontend/src/page/admin/users/index.js b/frontend/src/page/admin/users/index.js index 9e5c5f7253..8ff91b8a65 100644 --- a/frontend/src/page/admin/users/index.js +++ b/frontend/src/page/admin/users/index.js @@ -4,9 +4,9 @@ import UsersList from './UsersList'; import AdminMenu from '../admin-menu'; import PageContent from '../../../component/common/PageContent/PageContent'; -const render = () => ( +const render = ({history}) => (
- +