1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

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
This commit is contained in:
Ivar Conradi Østhus 2021-04-16 11:31:47 +02:00 committed by GitHub
parent 2a0acd3fb2
commit b3436b5ae6
19 changed files with 236 additions and 275 deletions

View File

@ -93,6 +93,15 @@ Array [
"title": "Sign out", "title": "Sign out",
"type": "protected", "type": "protected",
}, },
Object {
"component": [Function],
"hidden": false,
"icon": "album",
"layout": "main",
"path": "/admin",
"title": "Admin",
"type": "protected",
},
] ]
`; `;
@ -260,39 +269,6 @@ Array [
"title": "Projects", "title": "Projects",
"type": "protected", "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 { Object {
"component": [Function], "component": [Function],
"layout": "main", "layout": "main",
@ -389,5 +365,43 @@ Array [
"title": "Log in", "title": "Log in",
"type": "unprotected", "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",
},
] ]
`; `;

View File

@ -6,7 +6,7 @@ test('returns all defined routes', () => {
}); });
test('returns all baseRoutes', () => { test('returns all baseRoutes', () => {
expect(baseRoutes.length).toEqual(11); expect(baseRoutes.length).toEqual(12);
expect(baseRoutes).toMatchSnapshot(); expect(baseRoutes).toMatchSnapshot();
}); });

View File

@ -213,41 +213,6 @@ export const routes = [
layout: 'main', 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', path: '/tag-types/create',
parent: '/tag-types', parent: '/tag-types',
@ -342,6 +307,40 @@ export const routes = [
hidden: true, hidden: true,
layout: 'standalone', 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); export const getRoute = path => routes.find(route => route.path === path);

View File

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import HeaderTitle from '../../common/HeaderTitle'; import HeaderTitle from '../../common/HeaderTitle';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; 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 { Icon, IconButton, List, ListItem, ListItemAvatar, ListItemText, Tooltip } from '@material-ui/core';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import ConfirmDialogue from '../../common/Dialogue'; import ConfirmDialogue from '../../common/Dialogue';
@ -36,6 +36,16 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history, hasPermi
</Link> </Link>
); );
const mgmAccessButton = project => (
<Tooltip title="Manage access">
<Link to={`/projects/${project.id}/access`} style={{ color: 'black' }}>
<IconButton aria-label="manage_access" >
<Icon>supervised_user_circle</Icon>
</IconButton>
</Link>
</Tooltip>
);
const deleteProjectButton = project => ( const deleteProjectButton = project => (
<Tooltip title="Remove project"> <Tooltip title="Remove project">
<IconButton <IconButton
@ -57,12 +67,16 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history, hasPermi
<Icon>folder_open</Icon> <Icon>folder_open</Icon>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={projectLink(project)} secondary={project.description} /> <ListItemText primary={projectLink(project)} secondary={project.description} />
<ConditionallyRender
condition={hasPermission(UPDATE_PROJECT)}
show={mgmAccessButton(project)}
/>
<ConditionallyRender condition={hasPermission(DELETE_PROJECT)} show={deleteProjectButton(project)} /> <ConditionallyRender condition={hasPermission(DELETE_PROJECT)} show={deleteProjectButton(project)} />
</ListItem> </ListItem>
)); ));
return ( return (
<PageContent headerContent={<HeaderTitle title="Projects (beta)" actions={addProjectButton()} />}> <PageContent headerContent={<HeaderTitle title="Projects" actions={addProjectButton()} />}>
<List> <List>
<ConditionallyRender <ConditionallyRender
condition={projects.length > 0} condition={projects.length > 0}
@ -93,8 +107,6 @@ ProjectList.propTypes = {
removeProject: PropTypes.func.isRequired, removeProject: PropTypes.func.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
hasPermission: PropTypes.func.isRequired, hasPermission: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}; };
export default ProjectList; export default ProjectList;

View File

@ -1,16 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchProjects, removeProject } from '../../../store/project/actions'; import { fetchProjects, removeProject } from '../../../store/project/actions';
import { hasPermission } from '../../../permissions'; import { hasPermission } from '../../../permissions';
import { RBAC } from '../../common/flags';
import ProjectList from './ProjectList'; import ProjectList from './ProjectList';
const mapStateToProps = state => { const mapStateToProps = state => {
const projects = state.projects.toJS(); const projects = state.projects.toJS();
const rbacEnabled = !!state.uiConfig.toJS().flags[RBAC];
return { return {
projects, projects,
rbacEnabled,
hasPermission: hasPermission.bind(null, state.user.get('profile')), hasPermission: hasPermission.bind(null, state.user.get('profile')),
}; };
}; };

View File

@ -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 }) => (
<Link to={`/projects/edit/${id}`}>
<strong>{name}</strong>
</Link>
);
deleteProjectButton = project => (
<Tooltip title="Remove project">
<IconButton aria-label="delete" onClick={this.removeProject.bind(this, project)}>
<Icon>delete</Icon>
</IconButton>
</Tooltip>
);
projectList = () => {
const { projects, hasPermission } = this.props;
return projects.map((project, i) => (
<ListItem key={i}>
<ListItemAvatar>
<Icon>folder_open</Icon>
</ListItemAvatar>
<ListItemText primary={this.projectLink(project)} secondary={project.description} />
<ConditionallyRender
condition={hasPermission(DELETE_PROJECT)}
show={this.deleteProjectButton(project)}
/>
</ListItem>
));
};
addProjectButton = () => {
const { hasPermission } = this.props;
return (
<ConditionallyRender
condition={hasPermission(CREATE_PROJECT)}
show={
<Tooltip title="Add new project">
<IconButton
aria-label="add-project"
onClick={() => this.props.history.push('/projects/create')}
>
<Icon>add</Icon>
</IconButton>
</Tooltip>
}
/>
);
};
render() {
const { projects } = this.props;
return (
<Paper shadow={0} className={commonStyles.fullwidth}>
<HeaderTitle title="Projects (beta)" actions={this.addProjectButton()} />
<List>
<ConditionallyRender
condition={projects.length > 0}
show={this.projectList()}
elseShow={<ListItem>No projects defined</ListItem>}
/>
</List>
</Paper>
);
}
}
export default ProjectListComponent;

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { Grid, Icon } from '@material-ui/core'; import { Paper, Icon, Tabs, Tab } from '@material-ui/core';
import PageContent from '../../component/common/PageContent/PageContent';
const navLinkStyle = { const navLinkStyle = {
display: 'flex', display: 'flex',
@ -23,30 +22,36 @@ const iconStyle = {
marginRight: '5px', marginRight: '5px',
}; };
function AdminMenu() { function AdminMenu({history}) {
const { location } = history;
const { pathname } = location;
return ( return (
<PageContent style={{ marginBottom: '1rem' }}> <Paper style={{ marginBottom: '1rem' }}>
<Grid container justify={'center'}> <Tabs centered value={pathname} >
<Grid item md={4}> <Tab value="/admin/users" label={
<NavLink to="/admin/users" activeStyle={activeNavLinkStyle} style={navLinkStyle}> <NavLink to="/admin/users" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
<Icon style={iconStyle}>supervised_user_circle</Icon> <Icon style={iconStyle}>supervised_user_circle</Icon>
Users <span>Users</span>
</NavLink> </NavLink>
</Grid> }
<Grid item md={4}> >
</Tab>
<Tab value="/admin/api" label={
<NavLink to="/admin/api" activeStyle={activeNavLinkStyle} style={navLinkStyle}> <NavLink to="/admin/api" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
<Icon style={iconStyle}>apps</Icon> <Icon style={iconStyle}>apps</Icon>
API Access API Access
</NavLink> </NavLink>
</Grid> }>
<Grid item md={4}> </Tab>
<Tab value="/admin/auth" label={
<NavLink to="/admin/auth" activeStyle={activeNavLinkStyle} style={navLinkStyle}> <NavLink to="/admin/auth" activeStyle={activeNavLinkStyle} style={navLinkStyle}>
<Icon style={iconStyle}>lock</Icon> <Icon style={iconStyle}>lock</Icon>
Authentication Authentication
</NavLink> </NavLink>
</Grid> }>
</Grid> </Tab>
</PageContent> </Tabs>
</Paper>
); );
} }

View File

@ -1,25 +0,0 @@
import React from 'react';
function ApiHowTo() {
return (
<div style={{ marginBottom: '1rem' }}>
<p
style={{
backgroundColor: '#cfe5ff',
border: '2px solid #c4e1ff',
padding: '8px',
borderRadius: '5px',
}}
>
Read the{' '}
<a href="https://www.unleash-hosted.com/docs" target="_blank" rel="noreferrer">
Getting started guide
</a>{' '}
to learn how to connect to the Unleash API form your application or programmatically. <br /> <br />
Please note it can take up to 1 minute before a new API key is activated.
</p>
</div>
);
}
export default ApiHowTo;

View File

@ -7,6 +7,7 @@ import { hasPermission } from '../../../permissions';
export default connect( export default connect(
state => ({ state => ({
location: state.settings.toJS().location || {}, location: state.settings.toJS().location || {},
unleashUrl: state.uiConfig.toJS().unleashUrl,
keys: state.apiAdmin.toJS(), keys: state.apiAdmin.toJS(),
hasPermission: permission => hasPermission(state.user.get('profile'), permission), hasPermission: permission => hasPermission(state.user.get('profile'), permission),
}), }),

View File

@ -2,14 +2,14 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Icon, Table, TableHead, TableBody, TableRow, TableCell, IconButton } from '@material-ui/core'; 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 { formatFullDateTimeWithLocale } from '../../../component/common/util';
import CreateApiKey from './api-key-create'; import CreateApiKey from './api-key-create';
import Secret from './secret'; import Secret from './secret';
import ApiHowTo from './api-howto';
import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender';
import Dialogue from '../../../component/common/Dialogue/Dialogue'; 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 [showDelete, setShowDelete] = useState(false);
const [delKey, setDelKey] = useState(undefined); const [delKey, setDelKey] = useState(undefined);
const deleteKey = async () => { const deleteKey = async () => {
@ -25,7 +25,22 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermis
return ( return (
<div> <div>
<ApiHowTo /> <Alert severity="info" >
<p>
Read the{' '}
<a href="https://docs.getunleash.io/docs" target="_blank" rel="noreferrer">
Getting started guide
</a>{' '}
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.
</p>
<br />
<strong>API URL: </strong> <pre style={{display: 'inline'}}>{unleashUrl}/api/</pre>
</Alert>
<br /><br />
<br />
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
@ -93,6 +108,7 @@ ApiKeyList.propTypes = {
removeKey: PropTypes.func.isRequired, removeKey: PropTypes.func.isRequired,
addKey: PropTypes.func.isRequired, addKey: PropTypes.func.isRequired,
keys: PropTypes.array.isRequired, keys: PropTypes.array.isRequired,
unleashUrl: PropTypes.string,
hasPermission: PropTypes.func.isRequired, hasPermission: PropTypes.func.isRequired,
}; };

View File

@ -5,9 +5,9 @@ import ApiKeyList from './api-key-list-container';
import AdminMenu from '../admin-menu'; import AdminMenu from '../admin-menu';
import PageContent from '../../../component/common/PageContent/PageContent'; import PageContent from '../../../component/common/PageContent/PageContent';
const render = () => ( const render = ({history}) => (
<div> <div>
<AdminMenu /> <AdminMenu history={history} />
<PageContent headerContent="API Access"> <PageContent headerContent="API Access">
<ApiKeyList /> <ApiKeyList />
</PageContent> </PageContent>

View File

@ -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: <SamlAuth />,
},
{
label: 'Google',
component: <GoogleAuth />,
},
];
return (
<div>
<AdminMenu history={history} />
<PageContent headerContent="Authentication">
<ConditionallyRender condition={authenticationType === 'enterprise'}
show={
<TabNav tabData={tabs} />
}
/>
<ConditionallyRender condition={authenticationType === 'open-source'}
show={
<Alert severity="warning">
You are running the open-source version of Unleash. You have to use the Enterprise edition
in order configure Single Sign-on.</Alert>
}
/>
<ConditionallyRender condition={authenticationType === 'custom'}
show={
<Alert severity="warning">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.</Alert>
}
/>
</PageContent>
</div>
);
}
AdminAuthPage.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
authenticationType: PropTypes.string,
};
export default AdminAuthPage;

View File

@ -5,6 +5,7 @@ import { hasPermission } from '../../../permissions';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
config: state.authAdmin.get('google'), config: state.authAdmin.get('google'),
unleashUrl: state.uiConfig.toJS().unleashUrl,
hasPermission: permission => hasPermission(state.user.get('profile'), permission), hasPermission: permission => hasPermission(state.user.get('profile'), permission),
}); });

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; 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'; import PageContent from '../../../component/common/PageContent/PageContent';
const initialState = { const initialState = {
@ -9,7 +10,7 @@ const initialState = {
unleashHostname: location.hostname, unleashHostname: location.hostname,
}; };
function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission }) { function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission, unleashUrl }) {
const [data, setData] = useState(initialState); const [data, setData] = useState(initialState);
const [info, setInfo] = useState(); const [info, setInfo] = useState();
@ -58,14 +59,14 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission
<PageContent> <PageContent>
<Grid container style={{ marginBottom: '1rem' }}> <Grid container style={{ marginBottom: '1rem' }}>
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="subtitle1"> <Alert severity="info">
Please read the{' '} Please read the{' '}
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication/google" target="_blank" rel="noreferrer"> <a href="https://www.unleash-hosted.com/docs/enterprise-authentication/google" target="_blank" rel="noreferrer">
documentation documentation
</a>{' '} </a>{' '}
to learn how to integrate with Google OAuth 2.0. <br /> to learn how to integrate with Google OAuth 2.0. <br />
Callback URL: <code>https://[unleash.hostname.com]/auth/google/callback</code> Callback URL: <code>{unleashUrl}/auth/google/callback</code>
</Typography> </Alert>
</Grid> </Grid>
</Grid> </Grid>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
@ -189,6 +190,7 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission
GoogleAuth.propTypes = { GoogleAuth.propTypes = {
config: PropTypes.object, config: PropTypes.object,
unleashUrl: PropTypes.string,
getGoogleConfig: PropTypes.func.isRequired, getGoogleConfig: PropTypes.func.isRequired,
updateGoogleConfig: PropTypes.func.isRequired, updateGoogleConfig: PropTypes.func.isRequired,
hasPermission: PropTypes.func.isRequired, hasPermission: PropTypes.func.isRequired,

View File

@ -1,36 +1,12 @@
import React from 'react'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import component from './authentication';
import AdminMenu from '../admin-menu'; import { hasPermission } from '../../../permissions';
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';
function AdminAuthPage() { const mapStateToProps = state => ({
const tabs = [ authenticationType: state.uiConfig.toJS().authenticationType,
{ hasPermission: permission => hasPermission(state.user.get('profile'), permission),
label: 'SAML 2.0', });
component: <SamlAuth />,
},
{
label: 'Google',
component: <GoogleAuth />,
},
];
return ( const Container = connect(mapStateToProps, { })(component);
<div>
<AdminMenu />
<PageContent headerContent="Authentication">
<TabNav tabData={tabs} />
</PageContent>
</div>
);
}
AdminAuthPage.propTypes = { export default Container;
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default AdminAuthPage;

View File

@ -5,6 +5,7 @@ import { hasPermission } from '../../../permissions';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
config: state.authAdmin.get('saml'), config: state.authAdmin.get('saml'),
unleashUrl: state.uiConfig.toJS().unleashUrl,
hasPermission: permission => hasPermission(state.user.get('profile'), permission), hasPermission: permission => hasPermission(state.user.get('profile'), permission),
}); });

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; 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'; import PageContent from '../../../component/common/PageContent/PageContent';
const initialState = { const initialState = {
@ -9,7 +10,7 @@ const initialState = {
unleashHostname: location.hostname, unleashHostname: location.hostname,
}; };
function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) { function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission, unleashUrl }) {
const [data, setData] = useState(initialState); const [data, setData] = useState(initialState);
const [info, setInfo] = useState(); const [info, setInfo] = useState();
@ -26,7 +27,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
}, [config]); }, [config]);
if (!hasPermission('ADMIN')) { if (!hasPermission('ADMIN')) {
return <span>You need admin privileges to access this section.</span>; return <Alert severity="error">You need to be a root admin to access this section.</Alert>;
} }
const updateField = e => { const updateField = e => {
@ -59,14 +60,14 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
<PageContent> <PageContent>
<Grid container style={{ marginBottom: '1rem' }}> <Grid container style={{ marginBottom: '1rem' }}>
<Grid item md={12}> <Grid item md={12}>
<Typography variant="subtitle1"> <Alert severity="info">
Please read the{' '} Please read the{' '}
<a href="https://www.unleash-hosted.com/docs/enterprise-authentication" target="_blank" rel="noreferrer"> <a href="https://www.unleash-hosted.com/docs/enterprise-authentication" target="_blank" rel="noreferrer">
documentation documentation
</a>{' '} </a>{' '}
to learn how to integrate with specific SAML 2.0 providers (Okta, Keycloak, etc). <br /> to learn how to integrate with specific SAML 2.0 providers (Okta, Keycloak, etc). <br />
Callback URL: <code>https://[unleash.hostname.com]/auth/saml/callback</code> Callback URL: <code>{unleashUrl}/auth/saml/callback</code>
</Typography> </Alert>
</Grid> </Grid>
</Grid> </Grid>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
@ -184,6 +185,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) {
SamlAuth.propTypes = { SamlAuth.propTypes = {
config: PropTypes.object, config: PropTypes.object,
unleash: PropTypes.string,
getSamlConfig: PropTypes.func.isRequired, getSamlConfig: PropTypes.func.isRequired,
updateSamlConfig: PropTypes.func.isRequired, updateSamlConfig: PropTypes.func.isRequired,
hasPermission: PropTypes.func.isRequired, hasPermission: PropTypes.func.isRequired,

View File

@ -1,8 +1,8 @@
/* eslint-disable no-alert */ /* eslint-disable no-alert */
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core'; import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow, Avatar } from '@material-ui/core';
import { formatFullDateTimeWithLocale } from '../../../../component/common/util'; import { formatDateWithLocale } from '../../../../component/common/util';
import AddUser from '../add-user-component'; import AddUser from '../add-user-component';
import ChangePassword from '../change-password-component'; import ChangePassword from '../change-password-component';
import UpdateUser from '../update-user-component'; import UpdateUser from '../update-user-component';
@ -78,26 +78,26 @@ function UsersList({
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>Id</TableCell> <TableCell></TableCell>
<TableCell>Created</TableCell> <TableCell>Created</TableCell>
<TableCell>Username</TableCell>
<TableCell>Name</TableCell> <TableCell>Name</TableCell>
<TableCell>Role</TableCell> <TableCell>Username</TableCell>
<TableCell>{hasPermission('ADMIN') ? 'Action' : ''}</TableCell> <TableCell align="center">Role</TableCell>
<TableCell align="right">{hasPermission('ADMIN') ? 'Action' : ''}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{users.map(item => ( {users.map(item => (
<TableRow key={item.id}> <TableRow key={item.id}>
<TableCell>{item.id}</TableCell> <TableCell><Avatar variant="rounded" alt={item.name} src={item.imageUrl} title={`${item.name || item.email || item.username} (id: ${item.id})`} /></TableCell>
<TableCell>{formatFullDateTimeWithLocale(item.createdAt, location.locale)}</TableCell> <TableCell>{formatDateWithLocale(item.createdAt, location.locale)}</TableCell>
<TableCell style={{ textAlign: 'left' }}>{item.username || item.email}</TableCell>
<TableCell style={{ textAlign: 'left' }}>{item.name}</TableCell> <TableCell style={{ textAlign: 'left' }}>{item.name}</TableCell>
<TableCell>{renderRole(item.rootRole)}</TableCell> <TableCell style={{ textAlign: 'left' }}>{item.username || item.email}</TableCell>
<TableCell align="center">{renderRole(item.rootRole)}</TableCell>
<ConditionallyRender <ConditionallyRender
condition={hasPermission('ADMIN')} condition={hasPermission('ADMIN')}
show={ show={
<TableCell> <TableCell align="right">
<IconButton aria-label="Edit" title="Edit" onClick={openUpdateDialog(item)}> <IconButton aria-label="Edit" title="Edit" onClick={openUpdateDialog(item)}>
<Icon>edit</Icon> <Icon>edit</Icon>
</IconButton> </IconButton>

View File

@ -4,9 +4,9 @@ import UsersList from './UsersList';
import AdminMenu from '../admin-menu'; import AdminMenu from '../admin-menu';
import PageContent from '../../../component/common/PageContent/PageContent'; import PageContent from '../../../component/common/PageContent/PageContent';
const render = () => ( const render = ({history}) => (
<div> <div>
<AdminMenu /> <AdminMenu history={history} />
<PageContent headerContent="Users"> <PageContent headerContent="Users">
<UsersList /> <UsersList />
</PageContent> </PageContent>