1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01:00

Fix/login redirect (#442)

* fix: use swr with login

* fix: remove metrics poller

* fix: do not allow retry on 401

* fix: create swr provider

* fix: move accessprovider

* fix: remove metrics poller test

* fix: hide password auth if disableDefault is set

* Update src/component/project/ProjectList/ProjectList.tsx

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>

* fix: console log

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
Fredrik Strand Oseberg 2021-10-19 13:08:25 +02:00 committed by GitHub
parent ea2086a7f4
commit 005daa3740
69 changed files with 891 additions and 747 deletions

View File

@ -1,64 +0,0 @@
import configureStore from 'redux-mock-store';
import { List } from 'immutable';
import thunkMiddleware from 'redux-thunk';
import fetchMock from 'fetch-mock';
import MetricsPoller from '../metrics-poller';
const mockStore = configureStore([thunkMiddleware]);
describe('metrics-poller.js', () => {
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
test('Should not start poller before toggles are recieved', () => {
const initialState = { features: List.of([{ name: 'test1' }]) };
const store = mockStore(initialState);
fetchMock.getOnce('api/admin/metrics/feature-toggles', {
body: { lastHour: {}, lastMinute: {} },
headers: { 'content-type': 'application/json' },
});
const metricsPoller = new MetricsPoller(store);
metricsPoller.start();
expect(metricsPoller.timer).toBeUndefined();
});
test('Should not start poller when state does not contain toggles', () => {
const initialState = { features: new List([]) };
const store = mockStore(initialState);
const metricsPoller = new MetricsPoller(store);
metricsPoller.start();
store.dispatch({
type: 'some',
receivedAt: Date.now(),
});
expect(metricsPoller.timer).toBeUndefined();
});
test('Should start poller when state gets toggles', () => {
fetchMock.getOnce('api/admin/metrics/feature-toggles', {
body: { lastHour: {}, lastMinute: {} },
headers: { 'content-type': 'application/json' },
});
const initialState = { features: List.of([{ name: 'test1' }]) };
const store = mockStore(initialState);
const metricsPoller = new MetricsPoller(store);
metricsPoller.start();
store.dispatch({
type: 'RECEIVE_FEATURE_TOGGLES',
featureToggles: [{ name: 'test' }],
receivedAt: Date.now(),
});
expect(metricsPoller.timer).toBeDefined();
});
});

View File

@ -13,8 +13,8 @@ import IAuthStatus from '../interfaces/user';
import { useEffect } from 'react'; import { useEffect } from 'react';
import NotFound from './common/NotFound/NotFound'; import NotFound from './common/NotFound/NotFound';
import Feedback from './common/Feedback'; import Feedback from './common/Feedback';
import { SWRConfig } from 'swr';
import useToast from '../hooks/useToast'; import useToast from '../hooks/useToast';
import SWRProvider from './providers/SWRProvider/SWRProvider';
interface IAppProps extends RouteComponentProps { interface IAppProps extends RouteComponentProps {
user: IAuthStatus; user: IAuthStatus;
@ -75,55 +75,34 @@ const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
}; };
return ( return (
<SWRConfig <SWRProvider
value={{ setToastData={setToastData}
onErrorRetry: ( isUnauthorized={isUnauthorized}
error,
_key,
_config,
revalidate,
{ retryCount }
) => {
// Never retry on 404.
if (error.status === 404) {
return error;
}
setTimeout(() => revalidate({ retryCount }), 5000);
},
onError: error => {
if (!isUnauthorized()) {
setToastData({
show: true,
type: 'error',
text: error.message,
});
}
},
}}
> >
{' '}
<div className={styles.container}> <div className={styles.container}>
<LayoutPicker location={location}> <LayoutPicker location={location}>
<Switch> <Switch>
<ProtectedRoute <ProtectedRoute
exact exact
path='/' path="/"
unauthorized={isUnauthorized()} unauthorized={isUnauthorized()}
component={Redirect} component={Redirect}
renderProps={{ to: '/features' }} renderProps={{ to: '/features' }}
/> />
{renderMainLayoutRoutes()} {renderMainLayoutRoutes()}
{renderStandaloneRoutes()} {renderStandaloneRoutes()}
<Route path='/404' component={NotFound} /> <Route path="/404" component={NotFound} />
<Redirect to='/404' /> <Redirect to="/404" />
</Switch> </Switch>
<Feedback <Feedback
feedbackId='pnps' feedbackId="pnps"
openUrl='http://feedback.unleash.run' openUrl="http://feedback.unleash.run"
/> />
</LayoutPicker> </LayoutPicker>
{toast} {toast}
</div> </div>
</SWRConfig> </SWRProvider>
); );
}; };
// Set state to any for now, to avoid typing up entire state object while converting to tsx. // Set state to any for now, to avoid typing up entire state object while converting to tsx.

View File

@ -9,7 +9,7 @@ import {
ListItemText, ListItemText,
} from '@material-ui/core'; } from '@material-ui/core';
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
import { CREATE_ADDON } from '../../../AccessProvider/permissions'; import { CREATE_ADDON } from '../../../providers/AccessProvider/permissions';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => { const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => {

View File

@ -13,7 +13,7 @@ import ConditionallyRender from '../../../common/ConditionallyRender/Conditional
import { import {
DELETE_ADDON, DELETE_ADDON,
UPDATE_ADDON, UPDATE_ADDON,
} from '../../../AccessProvider/permissions'; } from '../../../providers/AccessProvider/permissions';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import PageContent from '../../../common/PageContent/PageContent'; import PageContent from '../../../common/PageContent/PageContent';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';

View File

@ -1,24 +1,37 @@
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button, IconButton, Table, TableBody, TableCell, TableHead, TableRow, } from '@material-ui/core'; import {
Button,
IconButton,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from '@material-ui/core';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import useLoading from '../../../hooks/useLoading'; import useLoading from '../../../hooks/useLoading';
import useApiTokens from '../../../hooks/api/getters/useApiTokens/useApiTokens'; import useApiTokens from '../../../hooks/api/getters/useApiTokens/useApiTokens';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import useApiTokensApi, { IApiTokenCreate } from '../../../hooks/api/actions/useApiTokensApi/useApiTokensApi'; import useApiTokensApi, {
IApiTokenCreate,
} from '../../../hooks/api/actions/useApiTokensApi/useApiTokensApi';
import ApiError from '../../common/ApiError/ApiError'; import ApiError from '../../common/ApiError/ApiError';
import PageContent from '../../common/PageContent'; import PageContent from '../../common/PageContent';
import HeaderTitle from '../../common/HeaderTitle'; import HeaderTitle from '../../common/HeaderTitle';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import { CREATE_API_TOKEN, DELETE_API_TOKEN } from '../../AccessProvider/permissions'; import {
CREATE_API_TOKEN,
DELETE_API_TOKEN,
} from '../../providers/AccessProvider/permissions';
import { useStyles } from './ApiTokenList.styles'; import { useStyles } from './ApiTokenList.styles';
import { formatDateWithLocale } from '../../common/util'; import { formatDateWithLocale } from '../../common/util';
import Secret from './secret'; import Secret from './secret';
import { Delete, FileCopy } from '@material-ui/icons'; import { Delete, FileCopy } from '@material-ui/icons';
import ApiTokenCreate from '../ApiTokenCreate/ApiTokenCreate'; import ApiTokenCreate from '../ApiTokenCreate/ApiTokenCreate';
import Dialogue from '../../common/Dialogue'; import Dialogue from '../../common/Dialogue';
import {CREATE_API_TOKEN_BUTTON} from '../../../testIds' import { CREATE_API_TOKEN_BUTTON } from '../../../testIds';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
interface IApiToken { interface IApiToken {
@ -54,7 +67,6 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
const closeDialog = () => { const closeDialog = () => {
setDialog(false); setDialog(false);
}; };
const renderError = () => { const renderError = () => {
return ( return (
@ -74,7 +86,7 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
show: true, show: true,
text: 'Successfully created API token.', text: 'Successfully created API token.',
}); });
} };
const copyToken = (value: string) => { const copyToken = (value: string) => {
navigator.clipboard.writeText(value); navigator.clipboard.writeText(value);
setToastData({ setToastData({
@ -85,7 +97,7 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
}; };
const onDeleteToken = async () => { const onDeleteToken = async () => {
if(delToken) { if (delToken) {
await deleteToken(delToken.secret); await deleteToken(delToken.secret);
} }
setDeleteToken(undefined); setDeleteToken(undefined);
@ -99,12 +111,12 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
}; };
const renderProject = (projectId: string) => { const renderProject = (projectId: string) => {
if(!projectId || projectId === '*') { if (!projectId || projectId === '*') {
return projectId; return projectId;
} else { } else {
return (<Link to={`/projects/${projectId}`}>{projectId}</Link>); return <Link to={`/projects/${projectId}`}>{projectId}</Link>;
} }
} };
const renderApiTokens = (tokens: IApiToken[]) => { const renderApiTokens = (tokens: IApiToken[]) => {
return ( return (
@ -112,65 +124,106 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell className={styles.hideSM}>Created</TableCell> <TableCell className={styles.hideSM}>Created</TableCell>
<TableCell className={styles.hideSM}>Username</TableCell> <TableCell className={styles.hideSM}>
<TableCell className={`${styles.center} ${styles.hideXS}`}>Type</TableCell> Username
<ConditionallyRender condition={uiConfig.flags.E} show={<> </TableCell>
<TableCell className={`${styles.center} ${styles.hideXS}`}>Project</TableCell> <TableCell
<TableCell className={`${styles.center} ${styles.hideXS}`}>Environment</TableCell> className={`${styles.center} ${styles.hideXS}`}
</>} /> >
Type
</TableCell>
<ConditionallyRender
condition={uiConfig.flags.E}
show={
<>
<TableCell
className={`${styles.center} ${styles.hideXS}`}
>
Project
</TableCell>
<TableCell
className={`${styles.center} ${styles.hideXS}`}
>
Environment
</TableCell>
</>
}
/>
<TableCell className={styles.hideMD}>Secret</TableCell> <TableCell className={styles.hideMD}>Secret</TableCell>
<TableCell className={styles.token}>Token</TableCell> <TableCell className={styles.token}>Token</TableCell>
<TableCell className={styles.actionsContainer}>Actions</TableCell> <TableCell className={styles.actionsContainer}>
Actions
</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{tokens.map(item => { {tokens.map(item => {
return ( return (
<TableRow key={item.secret}> <TableRow key={item.secret}>
<TableCell align="left" className={styles.hideSM}> <TableCell
align="left"
className={styles.hideSM}
>
{formatDateWithLocale( {formatDateWithLocale(
item.createdAt, item.createdAt,
location.locale location.locale
)} )}
</TableCell> </TableCell>
<TableCell align="left" className={styles.hideSM}> <TableCell
align="left"
className={styles.hideSM}
>
{item.username} {item.username}
</TableCell> </TableCell>
<TableCell className={`${styles.center} ${styles.hideXS}`}> <TableCell
className={`${styles.center} ${styles.hideXS}`}
>
{item.type} {item.type}
</TableCell> </TableCell>
<ConditionallyRender condition={uiConfig.flags.E} show={<> <ConditionallyRender
<TableCell className={`${styles.center} ${styles.hideXS}`}> condition={uiConfig.flags.E}
{renderProject(item.project)} show={
</TableCell> <>
<TableCell className={`${styles.center} ${styles.hideXS}`}> <TableCell
{item.environment} className={`${styles.center} ${styles.hideXS}`}
</TableCell> >
<TableCell className={styles.token}> {renderProject(item.project)}
<b>Type:</b> {item.type}<br/> </TableCell>
<b>Env:</b> {item.environment}<br/> <TableCell
<b>Project:</b> {renderProject(item.project)} className={`${styles.center} ${styles.hideXS}`}
</TableCell> >
</>} {item.environment}
elseShow={<> </TableCell>
<TableCell className={styles.token}> <TableCell className={styles.token}>
<b>Type:</b> {item.type}<br/> <b>Type:</b> {item.type}
<b>Username:</b> {item.username} <br />
</TableCell> <b>Env:</b> {item.environment}
</>} <br />
<b>Project:</b>{' '}
{renderProject(item.project)}
</TableCell>
</>
}
elseShow={
<>
<TableCell className={styles.token}>
<b>Type:</b> {item.type}
<br />
<b>Username:</b> {item.username}
</TableCell>
</>
}
/> />
<TableCell className={styles.hideMD}> <TableCell className={styles.hideMD}>
<Secret value={item.secret} /> <Secret value={item.secret} />
</TableCell> </TableCell>
<TableCell <TableCell className={styles.actionsContainer}>
className={styles.actionsContainer}
>
<IconButton <IconButton
onClick={() => { onClick={() => {
copyToken(item.secret) copyToken(item.secret);
} } }}
> >
<FileCopy /> <FileCopy />
</IconButton> </IconButton>
<ConditionallyRender <ConditionallyRender
condition={hasAccess(DELETE_API_TOKEN)} condition={hasAccess(DELETE_API_TOKEN)}
@ -179,29 +232,45 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
onClick={() => { onClick={() => {
setDeleteToken(item); setDeleteToken(item);
setShowDelete(true); setShowDelete(true);
} } }}
> >
<Delete /> <Delete />
</IconButton> </IconButton>
} /> }
/>
</TableCell> </TableCell>
</TableRow> </TableRow>
); );
})} })}
</TableBody> </TableBody>
</Table> </Table>
) );
} };
return ( return (
<div ref={ref}> <div ref={ref}>
<PageContent <PageContent
headerContent={<HeaderTitle headerContent={
title="API Access" <HeaderTitle
actions={<ConditionallyRender title="API Access"
condition={hasAccess(CREATE_API_TOKEN)} actions={
show={<Button variant="contained" color="primary" onClick={openDialog} data-test={CREATE_API_TOKEN_BUTTON}>Create API token</Button>} />} />} <ConditionallyRender
> condition={hasAccess(CREATE_API_TOKEN)}
show={
<Button
variant="contained"
color="primary"
onClick={openDialog}
data-test={CREATE_API_TOKEN_BUTTON}
>
Create API token
</Button>
}
/>
}
/>
}
>
<Alert severity="info" className={styles.infoBoxContainer}> <Alert severity="info" className={styles.infoBoxContainer}>
<p> <p>
Read the{' '} Read the{' '}
@ -218,7 +287,9 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
</p> </p>
<br /> <br />
<strong>API URL: </strong>{' '} <strong>API URL: </strong>{' '}
<pre style={{ display: 'inline' }}>{uiConfig.unleashUrl}/api/</pre> <pre style={{ display: 'inline' }}>
{uiConfig.unleashUrl}/api/
</pre>
</Alert> </Alert>
<ConditionallyRender condition={error} show={renderError()} /> <ConditionallyRender condition={error} show={renderError()} />
@ -230,7 +301,11 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
/> />
</div> </div>
{toast} {toast}
<ApiTokenCreate showDialog={showDialog} createToken={onCreateToken} closeDialog={closeDialog} /> <ApiTokenCreate
showDialog={showDialog}
createToken={onCreateToken}
closeDialog={closeDialog}
/>
<Dialogue <Dialogue
open={showDelete} open={showDelete}
onClick={onDeleteToken} onClick={onDeleteToken}
@ -241,10 +316,17 @@ const ApiTokenList = ({ location }: IApiTokenList) => {
title="Confirm deletion" title="Confirm deletion"
> >
<div> <div>
Are you sure you want to delete the following API token?<br /> Are you sure you want to delete the following API token?
<br />
<ul> <ul>
<li><strong>username</strong>: <code>{delToken?.username}</code></li> <li>
<li><strong>type</strong>: <code>{delToken?.type}</code></li> <strong>username</strong>:{' '}
<code>{delToken?.username}</code>
</li>
<li>
<strong>type</strong>:{' '}
<code>{delToken?.type}</code>
</li>
</ul> </ul>
</div> </div>
</Dialogue> </Dialogue>

View File

@ -4,11 +4,11 @@ import { ThemeProvider } from '@material-ui/core';
import ClientApplications from '../application-edit-component'; import ClientApplications from '../application-edit-component';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { ADMIN } from '../../AccessProvider/permissions'; import { ADMIN } from '../../providers/AccessProvider/permissions';
import theme from '../../../themes/main-theme'; import theme from '../../../themes/main-theme';
import { createFakeStore } from '../../../accessStoreFake'; import { createFakeStore } from '../../../accessStoreFake';
import AccessProvider from '../../AccessProvider/AccessProvider'; import AccessProvider from '../../providers/AccessProvider/AccessProvider';
test('renders correctly if no application', () => { test('renders correctly if no application', () => {
const tree = renderer const tree = renderer

View File

@ -18,7 +18,7 @@ import {
formatFullDateTimeWithLocale, formatFullDateTimeWithLocale,
formatDateWithLocale, formatDateWithLocale,
} from '../common/util'; } from '../common/util';
import { UPDATE_APPLICATION } from '../AccessProvider/permissions'; import { UPDATE_APPLICATION } from '../providers/AccessProvider/permissions';
import ApplicationView from './application-view'; import ApplicationView from './application-view';
import ApplicationUpdate from './application-update'; import ApplicationUpdate from './application-update';
import TabNav from '../common/TabNav/TabNav'; import TabNav from '../common/TabNav/TabNav';

View File

@ -13,7 +13,10 @@ import {
import { Report, Extension, Timeline } from '@material-ui/icons'; import { Report, Extension, Timeline } from '@material-ui/icons';
import { shorten } from '../common'; import { shorten } from '../common';
import { CREATE_FEATURE, CREATE_STRATEGY } from '../AccessProvider/permissions'; import {
CREATE_FEATURE,
CREATE_STRATEGY,
} from '../providers/AccessProvider/permissions';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
import { getTogglePath } from '../../utils/route-path-helpers'; import { getTogglePath } from '../../utils/route-path-helpers';
function ApplicationView({ function ApplicationView({
@ -33,9 +36,7 @@ function ApplicationView({
<Report /> <Report />
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={ primary={<Link to={`${createUrl}`}>{name}</Link>}
<Link to={`${createUrl}`}>{name}</Link>
}
secondary={'Missing, want to create?'} secondary={'Missing, want to create?'}
/> />
</ListItem> </ListItem>

View File

@ -5,7 +5,7 @@ import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyR
import { import {
CREATE_CONTEXT_FIELD, CREATE_CONTEXT_FIELD,
DELETE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD,
} from '../../AccessProvider/permissions'; } from '../../providers/AccessProvider/permissions';
import { import {
IconButton, IconButton,
List, List,

View File

@ -20,7 +20,7 @@ import AccessContext from '../../../../contexts/AccessContext';
import { import {
DELETE_ENVIRONMENT, DELETE_ENVIRONMENT,
UPDATE_ENVIRONMENT, UPDATE_ENVIRONMENT,
} from '../../../AccessProvider/permissions'; } from '../../../providers/AccessProvider/permissions';
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'; import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd';
import { XYCoord } from 'dnd-core'; import { XYCoord } from 'dnd-core';

View File

@ -30,7 +30,7 @@ const FeatureCreate = () => {
const { projectId } = useParams<IFeatureViewParams>(); const { projectId } = useParams<IFeatureViewParams>();
const { createFeatureToggle, validateFeatureToggleName } = useFeatureApi(); const { createFeatureToggle, validateFeatureToggleName } = useFeatureApi();
const history = useHistory(); const history = useHistory();
const [ toggle, setToggle ] = useState<IFeatureToggleDTO>({ const [toggle, setToggle] = useState<IFeatureToggleDTO>({
name: loadNameFromUrl(), name: loadNameFromUrl(),
description: '', description: '',
type: 'release', type: 'release',
@ -41,7 +41,6 @@ const FeatureCreate = () => {
}); });
const [errors, setErrors] = useState<Errors>({}); const [errors, setErrors] = useState<Errors>({});
useEffect(() => { useEffect(() => {
window.onbeforeunload = () => window.onbeforeunload = () =>
'Data will be lost if you leave the page, are you sure?'; 'Data will be lost if you leave the page, are you sure?';
@ -52,10 +51,7 @@ const FeatureCreate = () => {
}; };
}, []); }, []);
const onCancel = () => history.push( const onCancel = () => history.push(`/projects/${projectId}`);
`/projects/${projectId}`
);
const validateName = async (featureToggleName: string) => { const validateName = async (featureToggleName: string) => {
const e = { ...errors }; const e = { ...errors };
@ -80,25 +76,25 @@ const FeatureCreate = () => {
try { try {
await createFeatureToggle(projectId, toggle).then(() => await createFeatureToggle(projectId, toggle).then(() =>
history.push( history.push(getTogglePath(toggle.project, toggle.name, true))
getTogglePath(toggle.project, toggle.name, true)
)
); );
// Trigger // Trigger
} catch (e: any) { } catch (e: any) {
if (e.toString().includes('not allowed to be empty')) { if (e.toString().includes('not allowed to be empty')) {
setErrors({ name: 'Name is not allowed to be empty' }) setErrors({ name: 'Name is not allowed to be empty' });
} }
} }
}; };
const setValue = (field:string, value:string) => { const setValue = (field: string, value: string) => {
setToggle({...toggle, [field]: value}) setToggle({ ...toggle, [field]: value });
} };
return ( return (
<PageContent headerContent="Create feature toggle" bodyClass={styles.bodyContainer}> <PageContent
headerContent="Create feature toggle"
bodyClass={styles.bodyContainer}
>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<input type="hidden" name="project" value={projectId} /> <input type="hidden" name="project" value={projectId} />
<div className={styles.formContainer}> <div className={styles.formContainer}>

View File

@ -15,7 +15,7 @@ import HeaderTitle from '../../common/HeaderTitle';
import loadingFeatures from './loadingFeatures'; import loadingFeatures from './loadingFeatures';
import { CREATE_FEATURE } from '../../AccessProvider/permissions'; import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';

View File

@ -10,7 +10,7 @@ import TimeAgo from 'react-timeago';
import Status from '../../status-component'; import Status from '../../status-component';
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
import { UPDATE_FEATURE } from '../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../providers/AccessProvider/permissions';
import { styles as commonStyles } from '../../../common'; import { styles as commonStyles } from '../../../common';
import { useStyles } from './styles'; import { useStyles } from './styles';
@ -18,8 +18,6 @@ import { getTogglePath } from '../../../../utils/route-path-helpers';
import FeatureStatus from '../../FeatureView2/FeatureStatus/FeatureStatus'; import FeatureStatus from '../../FeatureView2/FeatureStatus/FeatureStatus';
import FeatureType from '../../FeatureView2/FeatureType/FeatureType'; import FeatureType from '../../FeatureView2/FeatureType/FeatureType';
const FeatureToggleListItem = ({ const FeatureToggleListItem = ({
feature, feature,
toggleFeature, toggleFeature,
@ -37,7 +35,7 @@ const FeatureToggleListItem = ({
const { name, description, type, stale, createdAt, project, lastSeenAt } = const { name, description, type, stale, createdAt, project, lastSeenAt } =
feature; feature;
return ( return (
<ListItem <ListItem
{...rest} {...rest}
@ -46,56 +44,60 @@ const FeatureToggleListItem = ({
<span className={styles.listItemMetric}> <span className={styles.listItemMetric}>
<FeatureStatus lastSeenAt={lastSeenAt} /> <FeatureStatus lastSeenAt={lastSeenAt} />
</span> </span>
<span className={classnames(styles.listItemType, commonStyles.hideLt600)}> <span
className={classnames(
styles.listItemType,
commonStyles.hideLt600
)}
>
<FeatureType type={type} /> <FeatureType type={type} />
</span> </span>
<span className={classnames(styles.listItemLink)}> <span className={classnames(styles.listItemLink)}>
<ConditionallyRender condition={!isArchive} show={ <ConditionallyRender
<Link condition={!isArchive}
to={getTogglePath(feature.project, name, flags.E)} show={
className={classnames( <Link
commonStyles.listLink, to={getTogglePath(feature.project, name, flags.E)}
commonStyles.truncate className={classnames(
)} commonStyles.listLink,
> commonStyles.truncate
<Tooltip title={description}> )}
<span className={commonStyles.toggleName}> >
<Tooltip title={description}>
<span className={commonStyles.toggleName}>
{name}&nbsp; {name}&nbsp;
</span> </span>
</Tooltip> </Tooltip>
<span className={styles.listItemToggle}> <span className={styles.listItemToggle}></span>
</span> <small>
<small> <TimeAgo date={createdAt} live={false} />
<TimeAgo date={createdAt} live={false} /> </small>
</small> <div>
<div> <span className={commonStyles.truncate}>
<span className={commonStyles.truncate}> <small>{description}</small>
<small>{description}</small> </span>
</span> </div>
</div> </Link>
</Link> }
} elseShow={ elseShow={
<> <>
<Tooltip title={description}> <Tooltip title={description}>
<span className={commonStyles.toggleName}> <span className={commonStyles.toggleName}>
{name}&nbsp; {name}&nbsp; z{' '}
z </span> </span>
</Tooltip> </Tooltip>
<span className={styles.listItemToggle}> <span className={styles.listItemToggle}></span>
</span> <small>
<small> <TimeAgo date={createdAt} live={false} />
<TimeAgo date={createdAt} live={false} /> </small>
</small> <div>
<div> <span className={commonStyles.truncate}>
<span className={commonStyles.truncate}> <small>{description}</small>
<small>{description}</small> </span>
</span> </div>
</div> </>
</> }
}/> />
</span> </span>
<span <span
className={classnames( className={classnames(
@ -104,8 +106,18 @@ z </span>
)} )}
> >
<Status stale={stale} showActive={false} /> <Status stale={stale} showActive={false} />
<Link to={`/projects/${project}`} style={{textDecoration: 'none'}}> <Link
<Chip color="primary" variant="outlined" className={styles.typeChip} style={{marginLeft: '8px' }} title={`Project: ${project}`} label={project}/> to={`/projects/${project}`}
style={{ textDecoration: 'none' }}
>
<Chip
color="primary"
variant="outlined"
className={styles.typeChip}
style={{ marginLeft: '8px' }}
title={`Project: ${project}`}
label={project}
/>
</Link> </Link>
</span> </span>
<ConditionallyRender <ConditionallyRender

View File

@ -6,8 +6,11 @@ import FeatureToggleList from '../FeatureToggleList';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import theme from '../../../../themes/main-theme'; import theme from '../../../../themes/main-theme';
import { createFakeStore } from '../../../../accessStoreFake'; import { createFakeStore } from '../../../../accessStoreFake';
import { ADMIN, CREATE_FEATURE } from '../../../AccessProvider/permissions'; import {
import AccessProvider from '../../../AccessProvider/AccessProvider'; ADMIN,
CREATE_FEATURE,
} from '../../../providers/AccessProvider/permissions';
import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
jest.mock('../FeatureToggleListItem', () => ({ jest.mock('../FeatureToggleListItem', () => ({
__esModule: true, __esModule: true,

View File

@ -21,7 +21,7 @@ import {
CREATE_FEATURE, CREATE_FEATURE,
DELETE_FEATURE, DELETE_FEATURE,
UPDATE_FEATURE, UPDATE_FEATURE,
} from '../../AccessProvider/permissions'; } from '../../providers/AccessProvider/permissions';
import StatusComponent from '../status-component'; import StatusComponent from '../status-component';
import FeatureTagComponent from '../feature-tag-component'; import FeatureTagComponent from '../feature-tag-component';
import StatusUpdateComponent from '../view/status-update-component'; import StatusUpdateComponent from '../view/status-update-component';

View File

@ -3,7 +3,7 @@ import classnames from 'classnames';
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature'; import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { IFeatureViewParams } from '../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../interfaces/params';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
import { useState } from 'react'; import { useState } from 'react';
import StaleDialog from './StaleDialog/StaleDialog'; import StaleDialog from './StaleDialog/StaleDialog';
import PermissionButton from '../../../../common/PermissionButton/PermissionButton'; import PermissionButton from '../../../../common/PermissionButton/PermissionButton';

View File

@ -31,7 +31,6 @@ const FeatureOverviewEnvironment = ({
return strategies.map(strategy => { return strategies.map(strategy => {
return ( return (
<FeatureOverviewStrategyCard <FeatureOverviewStrategyCard
data-loading
strategy={strategy} strategy={strategy}
key={strategy.id} key={strategy.id}
onClick={handleClick} onClick={handleClick}

View File

@ -2,7 +2,7 @@ import { Add } from '@material-ui/icons';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature'; import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
import { IFeatureViewParams } from '../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../interfaces/params';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
import ResponsiveButton from '../../../../common/ResponsiveButton/ResponsiveButton'; import ResponsiveButton from '../../../../common/ResponsiveButton/ResponsiveButton';
import FeatureOverviewEnvironment from './FeatureOverviewEnvironment/FeatureOverviewEnvironment'; import FeatureOverviewEnvironment from './FeatureOverviewEnvironment/FeatureOverviewEnvironment';
import { useStyles } from './FeatureOverviewStrategies.styles'; import { useStyles } from './FeatureOverviewStrategies.styles';

View File

@ -1,4 +1,4 @@
import { useState, useContext } from 'react'; import { useState, useContext } from 'react';
import { Chip } from '@material-ui/core'; import { Chip } from '@material-ui/core';
import { Add, Label } from '@material-ui/icons'; import { Add, Label } from '@material-ui/icons';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@ -16,7 +16,10 @@ import AddTagDialog from './AddTagDialog/AddTagDialog';
import Dialogue from '../../../../common/Dialogue'; import Dialogue from '../../../../common/Dialogue';
import { ITag } from '../../../../../interfaces/tags'; import { ITag } from '../../../../../interfaces/tags';
import useToast from '../../../../../hooks/useToast'; import useToast from '../../../../../hooks/useToast';
import { UPDATE_FEATURE, DELETE_TAG } from '../../../../AccessProvider/permissions'; import {
UPDATE_FEATURE,
DELETE_TAG,
} from '../../../../providers/AccessProvider/permissions';
import PermissionIconButton from '../../../../common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from '../../../../common/PermissionIconButton/PermissionIconButton';
import ConditionallyRender from '../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../common/ConditionallyRender';
import AccessContext from '../../../../../contexts/AccessContext'; import AccessContext from '../../../../../contexts/AccessContext';
@ -105,10 +108,14 @@ const FeatureOverviewTags = () => {
data-loading data-loading
label={t.value} label={t.value}
key={`${t.type}:${t.value}`} key={`${t.type}:${t.value}`}
onDelete={canDeleteTag ? () => { onDelete={
setShowDelDialog(true); canDeleteTag
setSelectedTag({ type: t.type, value: t.value }); ? () => {
}: undefined} setShowDelDialog(true);
setSelectedTag({ type: t.type, value: t.value });
}
: undefined
}
/> />
); );

View File

@ -5,7 +5,7 @@ import PermissionButton from '../../../../common/PermissionButton/PermissionButt
import FeatureTypeSelect from './FeatureTypeSelect/FeatureTypeSelect'; import FeatureTypeSelect from './FeatureTypeSelect/FeatureTypeSelect';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import AccessContext from '../../../../../contexts/AccessContext'; import AccessContext from '../../../../../contexts/AccessContext';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature'; import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
import { IFeatureViewParams } from '../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../interfaces/params';
import useToast from '../../../../../hooks/useToast'; import useToast from '../../../../../hooks/useToast';

View File

@ -10,7 +10,7 @@ import { projectFilterGenerator } from '../../../../../utils/project-filter-gene
import { import {
CREATE_FEATURE, CREATE_FEATURE,
UPDATE_FEATURE, UPDATE_FEATURE,
} from '../../../../AccessProvider/permissions'; } from '../../../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../common/ConditionallyRender';
import PermissionButton from '../../../../common/PermissionButton/PermissionButton'; import PermissionButton from '../../../../common/PermissionButton/PermissionButton';
import FeatureProjectSelect from './FeatureProjectSelect/FeatureProjectSelect'; import FeatureProjectSelect from './FeatureProjectSelect/FeatureProjectSelect';

View File

@ -20,7 +20,7 @@ import NoItems from '../../../../common/NoItems/NoItems';
import ResponsiveButton from '../../../../common/ResponsiveButton/ResponsiveButton'; import ResponsiveButton from '../../../../common/ResponsiveButton/ResponsiveButton';
import { Add } from '@material-ui/icons'; import { Add } from '@material-ui/icons';
import AccessContext from '../../../../../contexts/AccessContext'; import AccessContext from '../../../../../contexts/AccessContext';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
import useQueryParams from '../../../../../hooks/useQueryParams'; import useQueryParams from '../../../../../hooks/useQueryParams';
const FeatureStrategiesEnvironments = () => { const FeatureStrategiesEnvironments = () => {
@ -178,8 +178,12 @@ const FeatureStrategiesEnvironments = () => {
// Check groupId // Check groupId
const cacheParamKeys = Object.keys(cachedStrategy?.parameters || {}); const cacheParamKeys = Object.keys(
const strategyParamKeys = Object.keys(strategy?.parameters || {}); cachedStrategy?.parameters || {}
);
const strategyParamKeys = Object.keys(
strategy?.parameters || {}
);
// Check length of parameters // Check length of parameters
if (cacheParamKeys.length !== strategyParamKeys.length) { if (cacheParamKeys.length !== strategyParamKeys.length) {
equal = false; equal = false;

View File

@ -22,7 +22,7 @@ import {
UPDATE_STRATEGY_BUTTON_ID, UPDATE_STRATEGY_BUTTON_ID,
} from '../../../../../../testIds'; } from '../../../../../../testIds';
import AccessContext from '../../../../../../contexts/AccessContext'; import AccessContext from '../../../../../../contexts/AccessContext';
import { UPDATE_FEATURE } from '../../../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
import useFeatureApi from '../../../../../../hooks/api/actions/useFeatureApi/useFeatureApi'; import useFeatureApi from '../../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
interface IFeatureStrategyEditable { interface IFeatureStrategyEditable {

View File

@ -8,10 +8,9 @@ import classnames from 'classnames';
import { Button, IconButton, Tooltip, useMediaQuery } from '@material-ui/core'; import { Button, IconButton, Tooltip, useMediaQuery } from '@material-ui/core';
import { DoubleArrow } from '@material-ui/icons'; import { DoubleArrow } from '@material-ui/icons';
import ConditionallyRender from '../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../common/ConditionallyRender';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
import AccessContext from '../../../../../contexts/AccessContext'; import AccessContext from '../../../../../contexts/AccessContext';
const FeatureStrategiesList = () => { const FeatureStrategiesList = () => {
const smallScreen = useMediaQuery('(max-width:700px)'); const smallScreen = useMediaQuery('(max-width:700px)');
const { expandedSidebar, setExpandedSidebar } = useContext( const { expandedSidebar, setExpandedSidebar } = useContext(
@ -53,7 +52,7 @@ const FeatureStrategiesList = () => {
const iconClasses = classnames(styles.icon, { const iconClasses = classnames(styles.icon, {
[styles.expandedIcon]: expandedSidebar, [styles.expandedIcon]: expandedSidebar,
}); });
return ( return (
<section className={classes}> <section className={classes}>
<ConditionallyRender <ConditionallyRender
@ -65,7 +64,14 @@ const FeatureStrategiesList = () => {
</div> </div>
} }
/> />
<Tooltip title={hasAccess(UPDATE_FEATURE) ? 'Click to open.' : 'You don\'t have access to perform this operation'} arrow> <Tooltip
title={
hasAccess(UPDATE_FEATURE)
? 'Click to open.'
: "You don't have access to perform this operation"
}
arrow
>
<span className={styles.iconButtonWrapper}> <span className={styles.iconButtonWrapper}>
<IconButton <IconButton
className={styles.iconButton} className={styles.iconButton}

View File

@ -14,7 +14,7 @@ import {
getFeatureStrategyIcon, getFeatureStrategyIcon,
getHumanReadbleStrategyName, getHumanReadbleStrategyName,
} from '../../../../../../utils/strategy-names'; } from '../../../../../../utils/strategy-names';
import { UPDATE_FEATURE } from '../../../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../../common/ConditionallyRender';
import { useStyles } from './FeatureStrategyCard.styles'; import { useStyles } from './FeatureStrategyCard.styles';
@ -40,7 +40,7 @@ const FeatureStrategyCard = ({
FeatureStrategiesUIContext FeatureStrategiesUIContext
); );
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const canUpdateFeature = hasAccess(UPDATE_FEATURE) const canUpdateFeature = hasAccess(UPDATE_FEATURE);
const handleClick = () => { const handleClick = () => {
const strategy = getStrategyObject(strategies, name, featureId); const strategy = getStrategyObject(strategies, name, featureId);
@ -83,7 +83,9 @@ const FeatureStrategyCard = ({
<IconButton <IconButton
className={styles.addButton} className={styles.addButton}
onClick={handleClick} onClick={handleClick}
data-test={`${ADD_NEW_STRATEGY_CARD_BUTTON_ID}-${index + 1}`} data-test={`${ADD_NEW_STRATEGY_CARD_BUTTON_ID}-${
index + 1
}`}
disabled={!canUpdateFeature} disabled={!canUpdateFeature}
> >
<Add /> <Add />

View File

@ -19,7 +19,7 @@ import FeatureStrategiesSeparator from '../../FeatureStrategiesEnvironments/Feat
import DefaultStrategy from '../../common/DefaultStrategy/DefaultStrategy'; import DefaultStrategy from '../../common/DefaultStrategy/DefaultStrategy';
import { ADD_CONSTRAINT_ID } from '../../../../../../testIds'; import { ADD_CONSTRAINT_ID } from '../../../../../../testIds';
import AccessContext from '../../../../../../contexts/AccessContext'; import AccessContext from '../../../../../../contexts/AccessContext';
import { UPDATE_FEATURE } from '../../../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../../providers/AccessProvider/permissions';
interface IFeatureStrategyAccordionBodyProps { interface IFeatureStrategyAccordionBodyProps {
strategy: IFeatureStrategy; strategy: IFeatureStrategy;

View File

@ -19,7 +19,7 @@ import { useParams } from 'react-router';
import { IFeatureViewParams } from '../../../../../interfaces/params'; import { IFeatureViewParams } from '../../../../../interfaces/params';
import AccessContext from '../../../../../contexts/AccessContext'; import AccessContext from '../../../../../contexts/AccessContext';
import FeatureVariantListItem from './FeatureVariantsListItem/FeatureVariantsListItem'; import FeatureVariantListItem from './FeatureVariantsListItem/FeatureVariantsListItem';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../../../common/ConditionallyRender'; import ConditionallyRender from '../../../../common/ConditionallyRender';
import useUnleashContext from '../../../../../hooks/api/getters/useUnleashContext/useUnleashContext'; import useUnleashContext from '../../../../../hooks/api/getters/useUnleashContext/useUnleashContext';
import GeneralSelect from '../../../../common/GeneralSelect/GeneralSelect'; import GeneralSelect from '../../../../common/GeneralSelect/GeneralSelect';
@ -121,8 +121,8 @@ const FeatureOverviewVariants = () => {
style={{ display: 'block', marginTop: '0.5rem' }} style={{ display: 'block', marginTop: '0.5rem' }}
> >
By overriding the stickiness you can control which parameter By overriding the stickiness you can control which parameter
is used to ensure consistent traffic is used to ensure consistent traffic allocation across
allocation across variants.{' '} variants.{' '}
<a <a
href="https://docs.getunleash.io/advanced/toggle_variants" href="https://docs.getunleash.io/advanced/toggle_variants"
target="_blank" target="_blank"

View File

@ -8,7 +8,7 @@ import useProject from '../../../hooks/api/getters/useProject/useProject';
import useTabs from '../../../hooks/useTabs'; import useTabs from '../../../hooks/useTabs';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import { IFeatureViewParams } from '../../../interfaces/params'; import { IFeatureViewParams } from '../../../interfaces/params';
import { UPDATE_FEATURE } from '../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../providers/AccessProvider/permissions';
import Dialogue from '../../common/Dialogue'; import Dialogue from '../../common/Dialogue';
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
import FeatureLog from './FeatureLog/FeatureLog'; import FeatureLog from './FeatureLog/FeatureLog';
@ -106,8 +106,8 @@ const FeatureView2 = () => {
return ( return (
<div> <div>
<p> <p>
The feature <strong>{featureId.substring(0,30)}</strong> does not exist. Do The feature <strong>{featureId.substring(0, 30)}</strong>{' '}
you want to &nbsp; does not exist. Do you want to &nbsp;
<Link to={getCreateTogglePath(projectId)}>create it</Link> <Link to={getCreateTogglePath(projectId)}>create it</Link>
&nbsp;? &nbsp;?
</p> </p>

View File

@ -16,7 +16,7 @@ import {
CF_NAME_ID, CF_NAME_ID,
CF_TYPE_ID, CF_TYPE_ID,
} from '../../../../testIds'; } from '../../../../testIds';
import { CREATE_FEATURE } from '../../../AccessProvider/permissions'; import { CREATE_FEATURE } from '../../../providers/AccessProvider/permissions';
import { projectFilterGenerator } from '../../../../utils/project-filter-generator'; import { projectFilterGenerator } from '../../../../utils/project-filter-generator';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import useQueryParams from '../../../../hooks/useQueryParams'; import useQueryParams from '../../../../hooks/useQueryParams';

View File

@ -8,7 +8,7 @@ import { useStyles } from './StrategyCardHeader.styles.js';
import { ReactComponent as ReorderIcon } from '../../../../../assets/icons/reorder.svg'; import { ReactComponent as ReorderIcon } from '../../../../../assets/icons/reorder.svg';
import ConditionallyRender from '../../../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../../common/ConditionallyRender/ConditionallyRender';
import AccessContext from '../../../../../contexts/AccessContext'; import AccessContext from '../../../../../contexts/AccessContext';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions'; import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
const StrategyCardHeader = ({ const StrategyCardHeader = ({
name, name,

View File

@ -11,11 +11,11 @@ import {
ADMIN, ADMIN,
DELETE_FEATURE, DELETE_FEATURE,
UPDATE_FEATURE, UPDATE_FEATURE,
} from '../../../AccessProvider/permissions'; } from '../../../providers/AccessProvider/permissions';
import theme from '../../../../themes/main-theme'; import theme from '../../../../themes/main-theme';
import { createFakeStore } from '../../../../accessStoreFake'; import { createFakeStore } from '../../../../accessStoreFake';
import AccessProvider from '../../../AccessProvider/AccessProvider'; import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
jest.mock('../update-strategies-container', () => ({ jest.mock('../update-strategies-container', () => ({
__esModule: true, __esModule: true,

View File

@ -14,7 +14,7 @@ import { ReactComponent as UnleashLogo } from '../../../assets/img/logo-dark-wit
import { useStyles } from './Header.styles'; import { useStyles } from './Header.styles';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import { useCommonStyles } from '../../../common.styles'; import { useCommonStyles } from '../../../common.styles';
import { ADMIN } from '../../AccessProvider/permissions'; import { ADMIN } from '../../providers/AccessProvider/permissions';
import useUser from '../../../hooks/api/getters/useUser/useUser'; import useUser from '../../../hooks/api/getters/useUser/useUser';
import { IPermission } from '../../../interfaces/user'; import { IPermission } from '../../../interfaces/user';
import NavigationMenu from './NavigationMenu/NavigationMenu'; import NavigationMenu from './NavigationMenu/NavigationMenu';

View File

@ -28,7 +28,7 @@ import AdminApi from '../../page/admin/api';
import AdminUsers from '../../page/admin/users'; import AdminUsers from '../../page/admin/users';
import AdminInvoice from '../../page/admin/invoice'; import AdminInvoice from '../../page/admin/invoice';
import AdminAuth from '../../page/admin/auth'; import AdminAuth from '../../page/admin/auth';
import Login from '../user/Login'; import Login from '../user/Login/Login';
import { P, C, E, EEA } from '../common/flags'; import { P, C, E, EEA } from '../common/flags';
import NewUser from '../user/NewUser'; import NewUser from '../user/NewUser';
import ResetPassword from '../user/ResetPassword/ResetPassword'; import ResetPassword from '../user/ResetPassword/ResetPassword';
@ -40,7 +40,7 @@ import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList'; import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment'; import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
import FeatureView2 from '../feature/FeatureView2/FeatureView2'; import FeatureView2 from '../feature/FeatureView2/FeatureView2';
import FeatureCreate from '../feature/FeatureCreate/FeatureCreate' import FeatureCreate from '../feature/FeatureCreate/FeatureCreate';
export const routes = [ export const routes = [
// Project // Project
@ -164,7 +164,7 @@ export const routes = [
layout: 'main', layout: 'main',
menu: { mobile: true }, menu: { mobile: true },
}, },
// Applications // Applications
{ {
path: '/applications/:name', path: '/applications/:name',
@ -305,8 +305,8 @@ export const routes = [
menu: {}, menu: {},
}, },
// Addons // Addons
{ {
path: '/addons/create/:provider', path: '/addons/create/:provider',
parent: '/addons', parent: '/addons',
title: 'Create', title: 'Create',

View File

@ -14,7 +14,7 @@ import PageContent from '../../../common/PageContent';
import ResponsiveButton from '../../../common/ResponsiveButton/ResponsiveButton'; import ResponsiveButton from '../../../common/ResponsiveButton/ResponsiveButton';
import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew'; import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew';
import { useStyles } from './ProjectFeatureToggles.styles'; import { useStyles } from './ProjectFeatureToggles.styles';
import { CREATE_FEATURE } from '../../../AccessProvider/permissions'; import { CREATE_FEATURE } from '../../../providers/AccessProvider/permissions';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
interface IProjectFeatureToggles { interface IProjectFeatureToggles {
@ -61,7 +61,10 @@ const ProjectFeatureToggles = ({
<ResponsiveButton <ResponsiveButton
onClick={() => onClick={() =>
history.push( history.push(
getCreateTogglePath(id, uiConfig.flags.E) getCreateTogglePath(
id,
uiConfig.flags.E
)
) )
} }
maxWidth="700px" maxWidth="700px"

View File

@ -6,7 +6,7 @@ import useLoading from '../../../hooks/useLoading';
import PageContent from '../../common/PageContent'; import PageContent from '../../common/PageContent';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import HeaderTitle from '../../common/HeaderTitle'; import HeaderTitle from '../../common/HeaderTitle';
import { UPDATE_PROJECT } from '../../AccessProvider/permissions'; import { UPDATE_PROJECT } from '../../providers/AccessProvider/permissions';
import ApiError from '../../common/ApiError/ApiError'; import ApiError from '../../common/ApiError/ApiError';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
@ -19,7 +19,6 @@ import EnvironmentDisableConfirm from './EnvironmentDisableConfirm/EnvironmentDi
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
export interface ProjectEnvironment { export interface ProjectEnvironment {
name: string; name: string;
enabled: boolean; enabled: boolean;
@ -51,7 +50,6 @@ const ProjectEnvironmentList = ({ projectId }: ProjectEnvironmentListProps) => {
const ref = useLoading(loading); const ref = useLoading(loading);
const styles = useStyles(); const styles = useStyles();
const refetch = () => { const refetch = () => {
refetchEnvs(); refetchEnvs();
refetchProject(); refetchProject();
@ -164,52 +162,62 @@ const ProjectEnvironmentList = ({ projectId }: ProjectEnvironmentListProps) => {
headerContent={ headerContent={
<HeaderTitle <HeaderTitle
title={`Configure environments for "${project?.name}" project`} title={`Configure environments for "${project?.name}" project`}
/>} />
}
> >
<ConditionallyRender condition={uiConfig.flags.E} show={ <ConditionallyRender
<div className={styles.container}> condition={uiConfig.flags.E}
<ConditionallyRender condition={error} show={renderError()} /> show={
<Alert severity="info" style={{marginBottom: '20px'}}> <div className={styles.container}>
<b>Important!</b> In order for your application to retrieve configured activation strategies for a specific environment, <ConditionallyRender
the application<br/> must use an environment specific API key. You can look up the environment-specific API keys {' '} condition={error}
<Link show={renderError()}
to='/admin/api' />
<Alert
severity="info"
style={{ marginBottom: '20px' }}
> >
here. <b>Important!</b> In order for your application
</Link> to retrieve configured activation strategies for
<br/> a specific environment, the application
<br/> <br /> must use an environment specific API key.
Your administrator can configure an environment-specific API key to be used in the SDK. You can look up the environment-specific API
If you are an administrator you can {' '} keys <Link to="/admin/api">here.</Link>
<Link <br />
to='/admin/api' <br />
> Your administrator can configure an
create a new API key. environment-specific API key to be used in the
</Link> SDK. If you are an administrator you can{' '}
<Link to="/admin/api">
create a new API key.
</Link>
</Alert>
<ConditionallyRender
condition={environments.length < 1 && !loading}
show={<div>No environments available.</div>}
elseShow={renderEnvironments()}
/>
<EnvironmentDisableConfirm
env={selectedEnv}
open={!!selectedEnv}
handleDisableEnvironment={
handleDisableEnvironment
}
handleCancelDisableEnvironment={
handleCancelDisableEnvironment
}
confirmName={confirmName}
setConfirmName={setConfirmName}
/>
</div>
}
elseShow={
<Alert security="success">
This feature has not been Unleashed for you yet.
</Alert> </Alert>
<ConditionallyRender }
condition={environments.length < 1 && !loading} />
show={<div>No environments available.</div>}
elseShow={renderEnvironments()}
/>
<EnvironmentDisableConfirm
env={selectedEnv}
open={!!selectedEnv}
handleDisableEnvironment={handleDisableEnvironment}
handleCancelDisableEnvironment={
handleCancelDisableEnvironment
}
confirmName={confirmName}
setConfirmName={setConfirmName}
/>
</div>
} elseShow={
<Alert security="success">
This feature has not been Unleashed for you yet.
</Alert>
} />
{toast} {toast}
</PageContent> </PageContent>
</div> </div>

View File

@ -14,7 +14,7 @@ import PageContent from '../../common/PageContent';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import HeaderTitle from '../../common/HeaderTitle'; import HeaderTitle from '../../common/HeaderTitle';
import ResponsiveButton from '../../common/ResponsiveButton/ResponsiveButton'; import ResponsiveButton from '../../common/ResponsiveButton/ResponsiveButton';
import { CREATE_PROJECT } from '../../AccessProvider/permissions'; import { CREATE_PROJECT } from '../../providers/AccessProvider/permissions';
import { Add } from '@material-ui/icons'; import { Add } from '@material-ui/icons';
import ApiError from '../../common/ApiError/ApiError'; import ApiError from '../../common/ApiError/ApiError';
@ -26,21 +26,21 @@ type projectMap = {
}; };
function resolveCreateButtonData(isOss: boolean, hasAccess: boolean) { function resolveCreateButtonData(isOss: boolean, hasAccess: boolean) {
if(isOss) { if (isOss) {
return { return {
title: 'You must be on a paid subscription to create new projects', title: 'You must be on a paid subscription to create new projects',
disabled: true disabled: true,
} };
} else if (!hasAccess) { } else if (!hasAccess) {
return { return {
title: 'You do not have permissions create new projects', title: 'You do not have permission to create new projects',
disabled: true disabled: true,
} };
} else { } else {
return { return {
title: 'Click to create a new project', title: 'Click to create a new project',
disabled: false disabled: false,
} };
} }
} }
@ -64,7 +64,10 @@ const ProjectListNew = () => {
setFetchedProjects(prev => ({ ...prev, [projectId]: true })); setFetchedProjects(prev => ({ ...prev, [projectId]: true }));
}; };
const createButtonData = resolveCreateButtonData(isOss(), hasAccess(CREATE_PROJECT)); const createButtonData = resolveCreateButtonData(
isOss(),
hasAccess(CREATE_PROJECT)
);
const renderError = () => { const renderError = () => {
return ( return (

View File

@ -9,7 +9,7 @@ import { trim } from '../common/util';
import PageContent from '../common/PageContent/PageContent'; import PageContent from '../common/PageContent/PageContent';
import AccessContext from '../../contexts/AccessContext'; import AccessContext from '../../contexts/AccessContext';
import ConditionallyRender from '../common/ConditionallyRender'; import ConditionallyRender from '../common/ConditionallyRender';
import { CREATE_PROJECT } from '../AccessProvider/permissions'; import { CREATE_PROJECT } from '../providers/AccessProvider/permissions';
import HeaderTitle from '../common/HeaderTitle'; import HeaderTitle from '../common/HeaderTitle';
import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
@ -28,29 +28,28 @@ const ProjectFormComponent = (props: ProjectFormComponentProps) => {
const { editMode } = props; const { editMode } = props;
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const [project, setProject ] = useState(props.project || {}); const [project, setProject] = useState(props.project || {});
const [errors, setErrors ] = useState<any>({}); const [errors, setErrors] = useState<any>({});
const { isOss, loading } = useUiConfig(); const { isOss, loading } = useUiConfig();
const ref = useLoading(loading); const ref = useLoading(loading);
useEffect(() => { useEffect(() => {
if(!project.id && props.project.id) { if (!project.id && props.project.id) {
setProject(props.project); setProject(props.project);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.project]); }, [props.project]);
const setValue = (field: string, value: string) => { const setValue = (field: string, value: string) => {
const p = {...project} const p = { ...project };
p[field] = value; p[field] = value;
setProject(p) setProject(p);
}; };
const validateId = async (id: string) => { const validateId = async (id: string) => {
if (editMode) return true; if (editMode) return true;
const e = {...errors}; const e = { ...errors };
try { try {
await props.validateId(id); await props.validateId(id);
e.id = undefined; e.id = undefined;
@ -58,14 +57,14 @@ const ProjectFormComponent = (props: ProjectFormComponentProps) => {
e.id = err.message; e.id = err.message;
} }
setErrors(e) setErrors(e);
if (e.id) return false; if (e.id) return false;
return true; return true;
}; };
const validateName = () => { const validateName = () => {
if (project.name.length === 0) { if (project.name.length === 0) {
setErrors({...errors, name: 'Name can not be empty.' }) setErrors({ ...errors, name: 'Name can not be empty.' });
return false; return false;
} }
return true; return true;
@ -111,91 +110,103 @@ const ProjectFormComponent = (props: ProjectFormComponentProps) => {
/> />
} }
> >
<ConditionallyRender
<ConditionallyRender condition={isOss()} show={ condition={isOss()}
<Alert data-loading severity="error"> show={
{submitText} project requires a paid version of Unleash. <Alert data-loading severity="error">
Check out <a href="https://www.getunleash.io" target="_blank" rel="noreferrer">getunleash.io</a>{' '} {submitText} project requires a paid version of
to learn more. Unleash. Check out{' '}
</Alert> <a
} elseShow={ href="https://www.getunleash.io"
<> target="_blank"
<Typography rel="noreferrer"
variant="subtitle1" >
style={{ marginBottom: '0.5rem' }} getunleash.io
> </a>{' '}
Projects allows you to group feature toggles together in the to learn more.
management UI. </Alert>
</Typography> }
<form elseShow={
data-loading <>
onSubmit={onSubmit} <Typography
className={classnames( variant="subtitle1"
commonStyles.contentSpacing, style={{ marginBottom: '0.5rem' }}
styles.formContainer >
)} Projects allows you to group feature toggles
> together in the management UI.
<TextField </Typography>
label="Project Id" <form
name="id" data-loading
placeholder="A-unique-key" onSubmit={onSubmit}
value={project.id} className={classnames(
error={!!errors.id} commonStyles.contentSpacing,
helperText={errors.id} styles.formContainer
disabled={editMode} )}
variant="outlined" >
size="small" <TextField
onBlur={v => validateId(v.target.value)} label="Project Id"
onChange={v => name="id"
setValue('id', trim(v.target.value)) placeholder="A-unique-key"
} value={project.id}
/> error={!!errors.id}
<br /> helperText={errors.id}
<TextField disabled={editMode}
label="Name" variant="outlined"
name="name" size="small"
placeholder="Project name" onBlur={v => validateId(v.target.value)}
value={project.name} onChange={v =>
error={!!errors.name} setValue('id', trim(v.target.value))
variant="outlined" }
size="small" />
helperText={errors.name} <br />
onChange={v => setValue('name', v.target.value)} <TextField
/> label="Name"
<TextField name="name"
className={commonStyles.fullwidth} placeholder="Project name"
placeholder="A short description" value={project.name}
maxRows={2} error={!!errors.name}
label="Description" variant="outlined"
error={!!errors.description} size="small"
helperText={errors.description} helperText={errors.name}
variant="outlined" onChange={v =>
size="small" setValue('name', v.target.value)
multiline }
value={project.description} />
onChange={v => <TextField
setValue('description', v.target.value) className={commonStyles.fullwidth}
} placeholder="A short description"
/> maxRows={2}
label="Description"
error={!!errors.description}
helperText={errors.description}
variant="outlined"
size="small"
multiline
value={project.description}
onChange={v =>
setValue('description', v.target.value)
}
/>
<ConditionallyRender <ConditionallyRender
condition={hasAccess(CREATE_PROJECT)} condition={hasAccess(CREATE_PROJECT)}
show={ show={
<div className={styles.formButtons}> <div className={styles.formButtons}>
<FormButtons <FormButtons
submitText={submitText} submitText={submitText}
onCancel={onCancel} onCancel={onCancel}
/> />
</div> </div>
} }
/> />
</form> </form>
</> </>
} /> }
/>
</PageContent> </PageContent>
</div> </div>
); );
} };
ProjectFormComponent.propTypes = { ProjectFormComponent.propTypes = {
project: PropTypes.object.isRequired, project: PropTypes.object.isRequired,

View File

@ -1,6 +1,6 @@
import { FC } from 'react'; import { FC } from 'react';
import AccessContext from '../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from './permissions'; import { ADMIN } from './permissions';
// TODO: Type up redux store // TODO: Type up redux store

View File

@ -0,0 +1,67 @@
import { USER_CACHE_KEY } from '../../../hooks/api/getters/useUser/useUser';
import { mutate, SWRConfig, useSWRConfig } from 'swr';
import { useHistory } from 'react-router';
import { IToast } from '../../../hooks/useToast';
interface ISWRProviderProps {
setToastData: (toastData: IToast) => void;
isUnauthorized: () => boolean;
}
const SWRProvider: React.FC<ISWRProviderProps> = ({
children,
setToastData,
isUnauthorized,
}) => {
const { cache } = useSWRConfig();
const history = useHistory();
const handleFetchError = error => {
if (error.status === 401) {
cache.clear();
const path = location.pathname;
mutate(USER_CACHE_KEY, { ...error.info }, false);
if (path === '/login') {
return;
}
history.push('/login');
return;
}
if (!isUnauthorized()) {
setToastData({
show: true,
type: 'error',
text: error.message,
});
}
};
return (
<SWRConfig
value={{
onErrorRetry: (
error,
_key,
_config,
revalidate,
{ retryCount }
) => {
// Never retry on 404 or 401.
if (error.status < 499) {
return error;
}
setTimeout(() => revalidate({ retryCount }), 5000);
},
onError: handleFetchError,
}}
>
{children}
</SWRConfig>
);
};
export default SWRProvider;

View File

@ -24,7 +24,7 @@ import {
import { import {
CREATE_STRATEGY, CREATE_STRATEGY,
DELETE_STRATEGY, DELETE_STRATEGY,
} from '../../AccessProvider/permissions'; } from '../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import PageContent from '../../common/PageContent/PageContent'; import PageContent from '../../common/PageContent/PageContent';

View File

@ -5,9 +5,9 @@ import { ThemeProvider } from '@material-ui/core';
import StrategiesListComponent from '../StrategiesList/StrategiesList'; import StrategiesListComponent from '../StrategiesList/StrategiesList';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import theme from '../../../themes/main-theme'; import theme from '../../../themes/main-theme';
import AccessProvider from '../../AccessProvider/AccessProvider'; import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import { createFakeStore } from '../../../accessStoreFake'; import { createFakeStore } from '../../../accessStoreFake';
import { ADMIN } from '../../AccessProvider/permissions'; import { ADMIN } from '../../providers/AccessProvider/permissions';
test('renders correctly with one strategy', () => { test('renders correctly with one strategy', () => {
const strategy = { const strategy = {

View File

@ -5,7 +5,7 @@ import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import theme from '../../../themes/main-theme'; import theme from '../../../themes/main-theme';
import { createFakeStore } from '../../../accessStoreFake'; import { createFakeStore } from '../../../accessStoreFake';
import AccessProvider from '../../AccessProvider/AccessProvider'; import AccessProvider from '../../providers/AccessProvider/AccessProvider';
test('renders correctly with one strategy', () => { test('renders correctly with one strategy', () => {
const strategy = { const strategy = {

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { Grid, Typography } from '@material-ui/core'; import { Grid, Typography } from '@material-ui/core';
import ShowStrategy from './show-strategy-component'; import ShowStrategy from './show-strategy-component';
import EditStrategy from './CreateStrategy'; import EditStrategy from './CreateStrategy';
import { UPDATE_STRATEGY } from '../AccessProvider/permissions'; import { UPDATE_STRATEGY } from '../providers/AccessProvider/permissions';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
import TabNav from '../common/TabNav/TabNav'; import TabNav from '../common/TabNav/TabNav';
import PageContent from '../common/PageContent/PageContent'; import PageContent from '../common/PageContent/PageContent';

View File

@ -19,7 +19,7 @@ import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyR
import { import {
CREATE_TAG_TYPE, CREATE_TAG_TYPE,
DELETE_TAG_TYPE, DELETE_TAG_TYPE,
} from '../../AccessProvider/permissions'; } from '../../providers/AccessProvider/permissions';
import Dialogue from '../../common/Dialogue/Dialogue'; import Dialogue from '../../common/Dialogue/Dialogue';
import useMediaQuery from '@material-ui/core/useMediaQuery'; import useMediaQuery from '@material-ui/core/useMediaQuery';

View File

@ -3,12 +3,12 @@ import { ThemeProvider } from '@material-ui/core';
import TagTypes from '../form-tag-type-component'; import TagTypes from '../form-tag-type-component';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import theme from '../../../themes/main-theme'; import theme from '../../../themes/main-theme';
import AccessProvider from '../../AccessProvider/AccessProvider'; import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import { createFakeStore } from '../../../accessStoreFake'; import { createFakeStore } from '../../../accessStoreFake';
import { import {
CREATE_TAG_TYPE, CREATE_TAG_TYPE,
UPDATE_TAG_TYPE, UPDATE_TAG_TYPE,
} from '../../AccessProvider/permissions'; } from '../../providers/AccessProvider/permissions';
jest.mock('@material-ui/core/TextField'); jest.mock('@material-ui/core/TextField');

View File

@ -6,14 +6,14 @@ import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from '@material-ui/styles'; import { ThemeProvider } from '@material-ui/styles';
import theme from '../../../themes/main-theme'; import theme from '../../../themes/main-theme';
import { createFakeStore } from '../../../accessStoreFake'; import { createFakeStore } from '../../../accessStoreFake';
import AccessProvider from '../../AccessProvider/AccessProvider'; import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import { import {
ADMIN, ADMIN,
CREATE_TAG_TYPE, CREATE_TAG_TYPE,
UPDATE_TAG_TYPE, UPDATE_TAG_TYPE,
DELETE_TAG_TYPE, DELETE_TAG_TYPE,
} from '../../AccessProvider/permissions'; } from '../../providers/AccessProvider/permissions';
test('renders an empty list correctly', () => { test('renders an empty list correctly', () => {
const tree = renderer.create( const tree = renderer.create(

View File

@ -12,7 +12,7 @@ import AccessContext from '../../contexts/AccessContext';
import { import {
CREATE_TAG_TYPE, CREATE_TAG_TYPE,
UPDATE_TAG_TYPE, UPDATE_TAG_TYPE,
} from '../AccessProvider/permissions'; } from '../providers/AccessProvider/permissions';
import ConditionallyRender from '../common/ConditionallyRender'; import ConditionallyRender from '../common/ConditionallyRender';
const AddTagTypeComponent = ({ const AddTagTypeComponent = ({

View File

@ -14,7 +14,10 @@ import {
} from '@material-ui/core'; } from '@material-ui/core';
import { Add, Label, Delete } from '@material-ui/icons'; import { Add, Label, Delete } from '@material-ui/icons';
import { CREATE_TAG, DELETE_TAG } from '../../AccessProvider/permissions'; import {
CREATE_TAG,
DELETE_TAG,
} from '../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import HeaderTitle from '../../common/HeaderTitle'; import HeaderTitle from '../../common/HeaderTitle';
import PageContent from '../../common/PageContent/PageContent'; import PageContent from '../../common/PageContent/PageContent';

View File

@ -0,0 +1,102 @@
import SimpleAuth from '../SimpleAuth/SimpleAuth';
import AuthenticationCustomComponent from '../authentication-custom-component';
import PasswordAuth from '../PasswordAuth/PasswordAuth';
import HostedAuth from '../HostedAuth/HostedAuth';
import DemoAuth from '../DemoAuth';
import {
SIMPLE_TYPE,
DEMO_TYPE,
PASSWORD_TYPE,
HOSTED_TYPE,
} from '../../../constants/authTypes';
import SecondaryLoginActions from '../common/SecondaryLoginActions/SecondaryLoginActions';
import useUser from '../../../hooks/api/getters/useUser/useUser';
import { IUser } from '../../../interfaces/user';
import { useHistory } from 'react-router';
import useQueryParams from '../../../hooks/useQueryParams';
import ConditionallyRender from '../../common/ConditionallyRender';
import { Alert } from '@material-ui/lab';
interface IAuthenticationProps {
insecureLogin: (path: string, user: IUser) => void;
passwordLogin: (path: string, user: IUser) => void;
demoLogin: (path: string, user: IUser) => void;
history: any;
}
const Authentication = ({
insecureLogin,
passwordLogin,
demoLogin,
}: IAuthenticationProps) => {
const { authDetails } = useUser();
const history = useHistory();
const params = useQueryParams();
const error = params.get('errorMsg');
if (!authDetails) return null;
let content;
if (authDetails.type === PASSWORD_TYPE) {
content = (
<>
<PasswordAuth
passwordLogin={passwordLogin}
authDetails={authDetails}
history={history}
/>
<ConditionallyRender
condition={!authDetails.disableDefault}
show={<SecondaryLoginActions />}
/>
</>
);
} else if (authDetails.type === SIMPLE_TYPE) {
content = (
<SimpleAuth
insecureLogin={insecureLogin}
authDetails={authDetails}
history={history}
/>
);
} else if (authDetails.type === DEMO_TYPE) {
content = (
<DemoAuth
demoLogin={demoLogin}
authDetails={authDetails}
history={history}
/>
);
} else if (authDetails.type === HOSTED_TYPE) {
content = (
<>
<HostedAuth
passwordLogin={passwordLogin}
authDetails={authDetails}
history={history}
/>
<ConditionallyRender
condition={!authDetails.disableDefault}
show={<SecondaryLoginActions />}
/>
</>
);
} else {
content = <AuthenticationCustomComponent authDetails={authDetails} />;
}
return (
<>
<div style={{ maxWidth: '350px' }}>
<ConditionallyRender
condition={Boolean(error)}
show={<Alert severity="error">{error}</Alert>}
/>
</div>
{content}
</>
);
};
export default Authentication;

View File

@ -1,10 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import AuthenticationComponent from './authentication-component'; import AuthenticationComponent from './Authentication';
import { import {
insecureLogin, insecureLogin,
passwordLogin, passwordLogin,
demoLogin, demoLogin,
} from '../../store/user/actions'; } from '../../../store/user/actions';
const mapDispatchToProps = (dispatch, props) => ({ const mapDispatchToProps = (dispatch, props) => ({
demoLogin: (path, user) => demoLogin(path, user)(dispatch), demoLogin: (path, user) => demoLogin(path, user)(dispatch),
@ -12,12 +12,4 @@ const mapDispatchToProps = (dispatch, props) => ({
passwordLogin: (path, user) => passwordLogin(path, user)(dispatch), passwordLogin: (path, user) => passwordLogin(path, user)(dispatch),
}); });
const mapStateToProps = state => ({ export default connect(null, mapDispatchToProps)(AuthenticationComponent);
user: state.user.toJS(),
flags: state.uiConfig.toJS().flags,
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(AuthenticationComponent);

View File

@ -79,50 +79,58 @@ const HostedAuth = ({ authDetails, passwordLogin }) => {
} }
/> />
<form onSubmit={handleSubmit} action={authDetails.path}> <ConditionallyRender
<Typography variant="subtitle2" className={styles.apiError}> condition={!authDetails.disableDefault}
{apiError} show={
</Typography> <form onSubmit={handleSubmit} action={authDetails.path}>
<div <Typography
className={classnames( variant="subtitle2"
styles.contentContainer, className={styles.apiError}
commonStyles.contentSpacingY
)}
>
<TextField
label="Username or email"
name="username"
type="string"
onChange={evt => setUsername(evt.target.value)}
value={username}
error={!!usernameError}
helperText={usernameError}
variant="outlined"
size="small"
/>
<TextField
label="Password"
onChange={evt => setPassword(evt.target.value)}
name="password"
type="password"
value={password}
error={!!passwordError}
helperText={passwordError}
variant="outlined"
size="small"
/>
<Grid container>
<Button
variant="contained"
color="primary"
type="submit"
className={styles.button}
> >
Sign in {apiError}
</Button> </Typography>
</Grid> <div
</div> className={classnames(
</form> styles.contentContainer,
commonStyles.contentSpacingY
)}
>
<TextField
label="Username or email"
name="username"
type="string"
onChange={evt => setUsername(evt.target.value)}
value={username}
error={!!usernameError}
helperText={usernameError}
variant="outlined"
size="small"
/>
<TextField
label="Password"
onChange={evt => setPassword(evt.target.value)}
name="password"
type="password"
value={password}
error={!!passwordError}
helperText={passwordError}
variant="outlined"
size="small"
/>
<Grid container>
<Button
variant="contained"
color="primary"
type="submit"
className={styles.button}
>
Sign in
</Button>
</Grid>
</div>
</form>
}
/>
</> </>
); );
}; };

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import AuthenticationContainer from '../authentication-container'; import AuthenticationContainer from '../Authentication';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import { useStyles } from './Login.styles'; import { useStyles } from './Login.styles';
@ -8,22 +8,21 @@ import useQueryParams from '../../../hooks/useQueryParams';
import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess'; import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess';
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout'; import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
import { DEMO_TYPE } from '../../../constants/authTypes'; import { DEMO_TYPE } from '../../../constants/authTypes';
import useUser from '../../../hooks/api/getters/useUser/useUser';
import { useHistory } from 'react-router';
const Login = ({ history, user, fetchUser }) => { const Login = () => {
const styles = useStyles(); const styles = useStyles();
const { permissions, authDetails } = useUser();
const query = useQueryParams(); const query = useQueryParams();
const history = useHistory();
useEffect(() => { useEffect(() => {
fetchUser(); if (permissions?.length > 0) {
/* eslint-disable-next-line */
}, []);
useEffect(() => {
if (user.permissions.length > 0) {
history.push('features'); history.push('features');
} }
/* eslint-disable-next-line */ /* eslint-disable-next-line */
}, [user.permissions]); }, [permissions.length]);
const resetPassword = query.get('reset') === 'true'; const resetPassword = query.get('reset') === 'true';
@ -31,7 +30,7 @@ const Login = ({ history, user, fetchUser }) => {
<StandaloneLayout> <StandaloneLayout>
<div className={styles.loginFormContainer}> <div className={styles.loginFormContainer}>
<ConditionallyRender <ConditionallyRender
condition={user?.authDetails?.type !== DEMO_TYPE} condition={authDetails?.type !== DEMO_TYPE}
show={ show={
<h2 className={styles.title}> <h2 className={styles.title}>
Login to continue the great work Login to continue the great work

View File

@ -1,14 +0,0 @@
import { connect } from 'react-redux';
import { fetchUser } from '../../../store/user/actions';
import Login from './Login';
const mapStateToProps = state => ({
user: state.user.toJS(),
flags: state.uiConfig.toJS().flags,
});
const mapDispatchToProps = {
fetchUser,
};
export default connect(mapStateToProps, mapDispatchToProps)(Login);

View File

@ -79,59 +79,67 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
const { usernameError, passwordError, apiError } = errors; const { usernameError, passwordError, apiError } = errors;
return ( return (
<form onSubmit={handleSubmit} action={authDetails.path}> <ConditionallyRender
<ConditionallyRender condition={!authDetails.disableDefault}
condition={apiError} show={
show={ <form onSubmit={handleSubmit} action={authDetails.path}>
<Alert severity="error" className={styles.apiError}> <ConditionallyRender
{apiError} condition={apiError}
</Alert> show={
} <Alert
/> severity="error"
className={styles.apiError}
>
{apiError}
</Alert>
}
/>
<div <div
className={classnames( className={classnames(
styles.contentContainer, styles.contentContainer,
commonStyles.contentSpacingY commonStyles.contentSpacingY
)} )}
> >
<TextField <TextField
label="Username or email" label="Username or email"
name="username" name="username"
type="string" type="string"
onChange={evt => setUsername(evt.target.value)} onChange={evt => setUsername(evt.target.value)}
value={username} value={username}
error={!!usernameError} error={!!usernameError}
helperText={usernameError} helperText={usernameError}
variant="outlined" variant="outlined"
autoComplete="true" autoComplete="true"
size="small" size="small"
data-test={LOGIN_EMAIL_ID} data-test={LOGIN_EMAIL_ID}
/> />
<TextField <TextField
label="Password" label="Password"
onChange={evt => setPassword(evt.target.value)} onChange={evt => setPassword(evt.target.value)}
name="password" name="password"
type="password" type="password"
value={password} value={password}
error={!!passwordError} error={!!passwordError}
helperText={passwordError} helperText={passwordError}
variant="outlined" variant="outlined"
autoComplete="true" autoComplete="true"
size="small" size="small"
data-test={LOGIN_PASSWORD_ID} data-test={LOGIN_PASSWORD_ID}
/> />
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
type="submit" type="submit"
style={{ width: '150px', margin: '1rem auto' }} style={{ width: '150px', margin: '1rem auto' }}
data-test={LOGIN_BUTTON} data-test={LOGIN_BUTTON}
> >
Sign in Sign in
</Button> </Button>
</div> </div>
</form> </form>
}
/>
); );
}; };

View File

@ -1,79 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import SimpleAuth from './SimpleAuth/SimpleAuth';
import AuthenticationCustomComponent from './authentication-custom-component';
import PasswordAuth from './PasswordAuth/PasswordAuth';
import HostedAuth from './HostedAuth/HostedAuth';
import DemoAuth from './DemoAuth';
import {
SIMPLE_TYPE,
DEMO_TYPE,
PASSWORD_TYPE,
HOSTED_TYPE,
} from '../../constants/authTypes';
import SecondaryLoginActions from './common/SecondaryLoginActions/SecondaryLoginActions';
class AuthComponent extends React.Component {
static propTypes = {
user: PropTypes.object.isRequired,
demoLogin: PropTypes.func.isRequired,
insecureLogin: PropTypes.func.isRequired,
passwordLogin: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
};
render() {
const authDetails = this.props.user.authDetails;
if (!authDetails) return null;
let content;
if (authDetails.type === PASSWORD_TYPE) {
content = (
<>
<PasswordAuth
passwordLogin={this.props.passwordLogin}
authDetails={authDetails}
history={this.props.history}
/>
<SecondaryLoginActions />
</>
);
} else if (authDetails.type === SIMPLE_TYPE) {
content = (
<SimpleAuth
insecureLogin={this.props.insecureLogin}
authDetails={authDetails}
history={this.props.history}
/>
);
} else if (authDetails.type === DEMO_TYPE) {
content = (
<DemoAuth
demoLogin={this.props.demoLogin}
authDetails={authDetails}
history={this.props.history}
/>
);
} else if (authDetails.type === HOSTED_TYPE) {
content = (
<>
<HostedAuth
passwordLogin={this.props.passwordLogin}
authDetails={authDetails}
history={this.props.history}
/>
<SecondaryLoginActions />
</>
);
} else {
content = (
<AuthenticationCustomComponent authDetails={authDetails} />
);
}
return <>{content}</>;
}
}
export default AuthComponent;

View File

@ -1,6 +1,8 @@
const handleErrorResponses = (target: string) => async (res: Response) => { const handleErrorResponses = (target: string) => async (res: Response) => {
if (!res.ok) { if (!res.ok) {
const error = new Error(`An error occurred while trying to get ${target}`); const error = new Error(
`An error occurred while trying to get ${target}`
);
// Try to resolve body, but don't rethrow res.json is not a function // Try to resolve body, but don't rethrow res.json is not a function
try { try {
// @ts-ignore // @ts-ignore
@ -16,6 +18,6 @@ const handleErrorResponses = (target: string) => async (res: Response) => {
throw error; throw error;
} }
return res; return res;
} };
export default handleErrorResponses; export default handleErrorResponses;

View File

@ -4,20 +4,23 @@ import { formatApiPath } from '../../../../utils/format-path';
import { IPermission } from '../../../../interfaces/user'; import { IPermission } from '../../../../interfaces/user';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
export const USER_CACHE_KEY = `api/admin/user`;
const useUser = () => { const useUser = () => {
const KEY = `api/admin/user`;
const fetcher = () => { const fetcher = () => {
const path = formatApiPath(`api/admin/user`); const path = formatApiPath(`api/admin/user`);
return fetch(path, { return fetch(path, {
method: 'GET', method: 'GET',
}).then(handleErrorResponses('User info')).then(res => res.json()); })
.then(handleErrorResponses('User info'))
.then(res => res.json());
}; };
const { data, error } = useSWR(KEY, fetcher); const { data, error } = useSWR(USER_CACHE_KEY, fetcher);
const [loading, setLoading] = useState(!error && !data); const [loading, setLoading] = useState(!error && !data);
const refetch = () => { const refetch = () => {
mutate(KEY); mutate(USER_CACHE_KEY);
}; };
useEffect(() => { useEffect(() => {
@ -28,6 +31,7 @@ const useUser = () => {
user: data?.user || {}, user: data?.user || {},
permissions: (data?.permissions || []) as IPermission[], permissions: (data?.permissions || []) as IPermission[],
feedback: data?.feedback || [], feedback: data?.feedback || [],
authDetails: data || {},
error, error,
loading, loading,
refetch, refetch,

View File

@ -15,11 +15,10 @@ import { StylesProvider } from '@material-ui/core/styles';
import mainTheme from './themes/main-theme'; import mainTheme from './themes/main-theme';
import store from './store'; import store from './store';
import MetricsPoller from './metrics-poller';
import App from './component/AppContainer'; import App from './component/AppContainer';
import ScrollToTop from './component/scroll-to-top'; import ScrollToTop from './component/scroll-to-top';
import { writeWarning } from './security-logger'; import { writeWarning } from './security-logger';
import AccessProvider from './component/AccessProvider/AccessProvider'; import AccessProvider from './component/providers/AccessProvider/AccessProvider';
import { getBasePath } from './utils/format-path'; import { getBasePath } from './utils/format-path';
let composeEnhancers; let composeEnhancers;
@ -38,8 +37,6 @@ const unleashStore = createStore(
store, store,
composeEnhancers(applyMiddleware(thunkMiddleware)) composeEnhancers(applyMiddleware(thunkMiddleware))
); );
const metricsPoller = new MetricsPoller(unleashStore);
metricsPoller.start();
ReactDOM.render( ReactDOM.render(
<Provider store={unleashStore}> <Provider store={unleashStore}>

View File

@ -1,31 +0,0 @@
import { fetchFeatureMetrics } from './store/feature-metrics/actions';
class MetricsPoller {
constructor(store) {
this.store = store;
this.timer = undefined;
}
start() {
this.store.subscribe(() => {
const features = this.store.getState().features;
if (!this.timer && features.size > 0) {
this.timer = setInterval(this.fetchMetrics.bind(this), 5000);
this.fetchMetrics();
}
});
}
fetchMetrics() {
this.store.dispatch(fetchFeatureMetrics());
}
destroy() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
}
}
export default MetricsPoller;

View File

@ -1,10 +1,16 @@
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, FormControlLabel, Grid, Switch, TextField } from '@material-ui/core'; import {
Button,
FormControlLabel,
Grid,
Switch,
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent'; import PageContent from '../../../component/common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/AccessProvider/permissions'; import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
const initialState = { const initialState = {
enabled: false, enabled: false,
@ -93,12 +99,14 @@ function GoogleAuth({
</Grid> </Grid>
<Grid item xs={6} style={{ padding: '20px' }}> <Grid item xs={6} style={{ padding: '20px' }}>
<FormControlLabel <FormControlLabel
control={ <Switch control={
onChange={updateEnabled} <Switch
value={data.enabled} onChange={updateEnabled}
name="enabled" value={data.enabled}
checked={data.enabled} name="enabled"
/>} checked={data.enabled}
/>
}
label={data.enabled ? 'Enabled' : 'Disabled'} label={data.enabled ? 'Enabled' : 'Disabled'}
/> />
</Grid> </Grid>

View File

@ -1,10 +1,16 @@
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, FormControlLabel, Grid, Switch, TextField } from '@material-ui/core'; import {
Button,
FormControlLabel,
Grid,
Switch,
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent'; import PageContent from '../../../component/common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/AccessProvider/permissions'; import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
import AutoCreateForm from './AutoCreateForm/AutoCreateForm'; import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
const initialState = { const initialState = {
@ -57,7 +63,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
...data, ...data,
[field]: value, [field]: value,
}); });
} };
const onSubmit = async e => { const onSubmit = async e => {
e.preventDefault(); e.preventDefault();
@ -100,12 +106,14 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
</Grid> </Grid>
<Grid item md={6} style={{ padding: '20px' }}> <Grid item md={6} style={{ padding: '20px' }}>
<FormControlLabel <FormControlLabel
control={ <Switch control={
onChange={updateEnabled} <Switch
value={data.enabled} onChange={updateEnabled}
name="enabled" value={data.enabled}
checked={data.enabled} name="enabled"
/>} checked={data.enabled}
/>
}
label={data.enabled ? 'Enabled' : 'Disabled'} label={data.enabled ? 'Enabled' : 'Disabled'}
/> />
</Grid> </Grid>
@ -125,7 +133,6 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
style={{ width: '400px' }} style={{ width: '400px' }}
variant="outlined" variant="outlined"
size="small" size="small"
/> />
</Grid> </Grid>
</Grid> </Grid>
@ -151,7 +158,9 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<strong>Client secret</strong> <strong>Client secret</strong>
<p>(Required) Client secret of your OpenID application. </p> <p>
(Required) Client secret of your OpenID application.{' '}
</p>
</Grid> </Grid>
<Grid item md={6}> <Grid item md={6}>
<TextField <TextField
@ -171,18 +180,27 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item md={5}> <Grid item md={5}>
<strong>(Optional) Enable Single Sign-Out</strong> <strong>(Optional) Enable Single Sign-Out</strong>
<p>If you enable Single Sign-Out Unleash will redirect the user to the IDP as part of the Sign-out process.</p> <p>
If you enable Single Sign-Out Unleash will redirect
the user to the IDP as part of the Sign-out process.
</p>
</Grid> </Grid>
<Grid item md={6} style={{ padding: '20px' }}> <Grid item md={6} style={{ padding: '20px' }}>
<FormControlLabel <FormControlLabel
control={ <Switch control={
onChange={updateSingleSignOut} <Switch
value={data.enableSingleSignOut} onChange={updateSingleSignOut}
disabled={!data.enabled} value={data.enableSingleSignOut}
name="enableSingleSignOut" disabled={!data.enabled}
checked={data.enableSingleSignOut} name="enableSingleSignOut"
/>} checked={data.enableSingleSignOut}
label={data.enableSingleSignOut ? 'Enabled' : 'Disabled'} />
}
label={
data.enableSingleSignOut
? 'Enabled'
: 'Disabled'
}
/> />
</Grid> </Grid>
</Grid> </Grid>
@ -199,7 +217,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
Save Save
</Button>{' '} </Button>{' '}
<small>{info}</small> <small>{info}</small>
<small style={{color: 'red'}}>{error}</small> <small style={{ color: 'red' }}>{error}</small>
</Grid> </Grid>
</Grid> </Grid>
</form> </form>

View File

@ -1,10 +1,16 @@
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, FormControlLabel, Grid, Switch, TextField } from '@material-ui/core'; import {
Button,
FormControlLabel,
Grid,
Switch,
TextField,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import PageContent from '../../../component/common/PageContent/PageContent'; import PageContent from '../../../component/common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/AccessProvider/permissions'; import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
import AutoCreateForm from './AutoCreateForm/AutoCreateForm'; import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
const initialState = { const initialState = {
@ -51,7 +57,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
...data, ...data,
[field]: value, [field]: value,
}); });
} };
const onSubmit = async e => { const onSubmit = async e => {
e.preventDefault(); e.preventDefault();
@ -92,12 +98,14 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
</Grid> </Grid>
<Grid item md={6}> <Grid item md={6}>
<FormControlLabel <FormControlLabel
control={ <Switch control={
onChange={updateEnabled} <Switch
value={data.enabled} onChange={updateEnabled}
name="enabled" value={data.enabled}
checked={data.enabled} name="enabled"
/>} checked={data.enabled}
/>
}
label={data.enabled ? 'Enabled' : 'Disabled'} label={data.enabled ? 'Enabled' : 'Disabled'}
/> />
</Grid> </Grid>
@ -136,7 +144,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
name="signOnUrl" name="signOnUrl"
value={data.signOnUrl || ''} value={data.signOnUrl || ''}
disabled={!data.enabled} disabled={!data.enabled}
style={{ width: '400px'}} style={{ width: '400px' }}
variant="outlined" variant="outlined"
size="small" size="small"
required required
@ -158,12 +166,13 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
name="certificate" name="certificate"
value={data.certificate || ''} value={data.certificate || ''}
disabled={!data.enabled} disabled={!data.enabled}
style={{width: '100%'}} style={{ width: '100%' }}
InputProps={{ InputProps={{
style: { style: {
fontSize: '0.6em', fontSize: '0.6em',
fontFamily: 'monospace', fontFamily: 'monospace',
}}} },
}}
multiline multiline
rows={14} rows={14}
rowsMax={14} rowsMax={14}
@ -189,7 +198,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
name="signOutUrl" name="signOutUrl"
value={data.signOutUrl || ''} value={data.signOutUrl || ''}
disabled={!data.enabled} disabled={!data.enabled}
style={{ width: '400px'}} style={{ width: '400px' }}
variant="outlined" variant="outlined"
size="small" size="small"
/> />
@ -199,8 +208,10 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
<Grid item md={5}> <Grid item md={5}>
<strong>Service Provider X.509 Certificate</strong> <strong>Service Provider X.509 Certificate</strong>
<p> <p>
(Optional) The private certificate used by the Service Provider used to sign the SAML 2.0 (Optional) The private certificate used by the
request towards the IDP. E.g. used to sign single logout requests (SLO). Service Provider used to sign the SAML 2.0 request
towards the IDP. E.g. used to sign single logout
requests (SLO).
</p> </p>
</Grid> </Grid>
<Grid item md={7}> <Grid item md={7}>
@ -210,12 +221,13 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
name="spCertificate" name="spCertificate"
value={data.spCertificate || ''} value={data.spCertificate || ''}
disabled={!data.enabled} disabled={!data.enabled}
style={{width: '100%'}} style={{ width: '100%' }}
InputProps={{ InputProps={{
style: { style: {
fontSize: '0.6em', fontSize: '0.6em',
fontFamily: 'monospace', fontFamily: 'monospace',
}}} },
}}
multiline multiline
rows={14} rows={14}
rowsMax={14} rowsMax={14}

View File

@ -2,7 +2,7 @@ import { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import InvoiceList from './invoice-container'; import InvoiceList from './invoice-container';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/AccessProvider/permissions'; import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
import ConditionallyRender from '../../../component/common/ConditionallyRender'; import ConditionallyRender from '../../../component/common/ConditionallyRender';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
@ -13,17 +13,13 @@ const InvoiceAdminPage = ({ history }) => {
<div> <div>
<ConditionallyRender <ConditionallyRender
condition={hasAccess(ADMIN)} condition={hasAccess(ADMIN)}
show={ show={<InvoiceList />}
<InvoiceList />
}
elseShow={ elseShow={
<Alert severity="error"> <Alert severity="error">
You need to be instance admin to access this section. You need to be instance admin to access this section.
</Alert> </Alert>
} }
/> />
</div> </div>
); );
}; };

View File

@ -7,7 +7,7 @@ import {
} from '@material-ui/core'; } from '@material-ui/core';
import { Edit, Lock, Delete } from '@material-ui/icons'; import { Edit, Lock, Delete } from '@material-ui/icons';
import { SyntheticEvent, useContext } from 'react'; import { SyntheticEvent, useContext } from 'react';
import { ADMIN } from '../../../../../component/AccessProvider/permissions'; import { ADMIN } from '../../../../../component/providers/AccessProvider/permissions';
import ConditionallyRender from '../../../../../component/common/ConditionallyRender'; import ConditionallyRender from '../../../../../component/common/ConditionallyRender';
import { formatDateWithLocale } from '../../../../../component/common/util'; import { formatDateWithLocale } from '../../../../../component/common/util';
import AccessContext from '../../../../../contexts/AccessContext'; import AccessContext from '../../../../../contexts/AccessContext';

View File

@ -14,7 +14,7 @@ import UpdateUser from '../update-user-component';
import DelUser from '../del-user-component'; import DelUser from '../del-user-component';
import ConditionallyRender from '../../../../component/common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../../component/common/ConditionallyRender/ConditionallyRender';
import AccessContext from '../../../../contexts/AccessContext'; import AccessContext from '../../../../contexts/AccessContext';
import { ADMIN } from '../../../../component/AccessProvider/permissions'; import { ADMIN } from '../../../../component/providers/AccessProvider/permissions';
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded'; import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
import useUsers from '../../../../hooks/api/getters/useUsers/useUsers'; import useUsers from '../../../../hooks/api/getters/useUsers/useUsers';
import useAdminUsersApi from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi'; import useAdminUsersApi from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';

View File

@ -5,7 +5,7 @@ import AdminMenu from '../admin-menu';
import PageContent from '../../../component/common/PageContent/PageContent'; import PageContent from '../../../component/common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import ConditionallyRender from '../../../component/common/ConditionallyRender'; import ConditionallyRender from '../../../component/common/ConditionallyRender';
import { ADMIN } from '../../../component/AccessProvider/permissions'; import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import HeaderTitle from '../../../component/common/HeaderTitle'; import HeaderTitle from '../../../component/common/HeaderTitle';
import { Button } from '@material-ui/core'; import { Button } from '@material-ui/core';

View File

@ -1,6 +1,6 @@
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { ADMIN } from '../../component/AccessProvider/permissions'; import { ADMIN } from '../../component/providers/AccessProvider/permissions';
import ConditionallyRender from '../../component/common/ConditionallyRender'; import ConditionallyRender from '../../component/common/ConditionallyRender';
import HistoryComponent from '../../component/history/EventHistory'; import HistoryComponent from '../../component/history/EventHistory';
import AccessContext from '../../contexts/AccessContext'; import AccessContext from '../../contexts/AccessContext';

View File

@ -1,4 +1,4 @@
import { ADMIN } from '../component/AccessProvider/permissions'; import { ADMIN } from '../component/providers/AccessProvider/permissions';
import IAuthStatus, { IPermission } from '../interfaces/user'; import IAuthStatus, { IPermission } from '../interfaces/user';
type objectIdx = { type objectIdx = {
@ -24,6 +24,6 @@ export const projectFilterGenerator = (
{} {}
); );
return (projectId: string) => { return (projectId: string) => {
return admin || permissionMap[projectId] return admin || permissionMap[projectId];
}; };
}; };