1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +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 NotFound from './common/NotFound/NotFound';
import Feedback from './common/Feedback';
import { SWRConfig } from 'swr';
import useToast from '../hooks/useToast';
import SWRProvider from './providers/SWRProvider/SWRProvider';
interface IAppProps extends RouteComponentProps {
user: IAuthStatus;
@ -75,55 +75,34 @@ const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
};
return (
<SWRConfig
value={{
onErrorRetry: (
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,
});
}
},
}}
<SWRProvider
setToastData={setToastData}
isUnauthorized={isUnauthorized}
>
{' '}
<div className={styles.container}>
<LayoutPicker location={location}>
<Switch>
<ProtectedRoute
exact
path='/'
path="/"
unauthorized={isUnauthorized()}
component={Redirect}
renderProps={{ to: '/features' }}
/>
{renderMainLayoutRoutes()}
{renderStandaloneRoutes()}
<Route path='/404' component={NotFound} />
<Redirect to='/404' />
<Route path="/404" component={NotFound} />
<Redirect to="/404" />
</Switch>
<Feedback
feedbackId='pnps'
openUrl='http://feedback.unleash.run'
feedbackId="pnps"
openUrl="http://feedback.unleash.run"
/>
</LayoutPicker>
{toast}
</div>
</SWRConfig>
</SWRProvider>
);
};
// 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,
} from '@material-ui/core';
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
import { CREATE_ADDON } from '../../../AccessProvider/permissions';
import { CREATE_ADDON } from '../../../providers/AccessProvider/permissions';
import PropTypes from 'prop-types';
const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ import {
CREATE_FEATURE,
DELETE_FEATURE,
UPDATE_FEATURE,
} from '../../AccessProvider/permissions';
} from '../../providers/AccessProvider/permissions';
import StatusComponent from '../status-component';
import FeatureTagComponent from '../feature-tag-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 { useParams } from 'react-router-dom';
import { IFeatureViewParams } from '../../../../../interfaces/params';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions';
import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
import { useState } from 'react';
import StaleDialog from './StaleDialog/StaleDialog';
import PermissionButton from '../../../../common/PermissionButton/PermissionButton';

View File

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

View File

@ -2,7 +2,7 @@ import { Add } from '@material-ui/icons';
import { Link, useParams } from 'react-router-dom';
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
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 FeatureOverviewEnvironment from './FeatureOverviewEnvironment/FeatureOverviewEnvironment';
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 { Add, Label } from '@material-ui/icons';
import { useParams } from 'react-router-dom';
@ -16,7 +16,10 @@ import AddTagDialog from './AddTagDialog/AddTagDialog';
import Dialogue from '../../../../common/Dialogue';
import { ITag } from '../../../../../interfaces/tags';
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 ConditionallyRender from '../../../../common/ConditionallyRender';
import AccessContext from '../../../../../contexts/AccessContext';
@ -105,10 +108,14 @@ const FeatureOverviewTags = () => {
data-loading
label={t.value}
key={`${t.type}:${t.value}`}
onDelete={canDeleteTag ? () => {
setShowDelDialog(true);
setSelectedTag({ type: t.type, value: t.value });
}: undefined}
onDelete={
canDeleteTag
? () => {
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 { useParams } from 'react-router';
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 { IFeatureViewParams } from '../../../../../interfaces/params';
import useToast from '../../../../../hooks/useToast';

View File

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

View File

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

View File

@ -22,7 +22,7 @@ import {
UPDATE_STRATEGY_BUTTON_ID,
} from '../../../../../../testIds';
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';
interface IFeatureStrategyEditable {

View File

@ -8,10 +8,9 @@ import classnames from 'classnames';
import { Button, IconButton, Tooltip, useMediaQuery } from '@material-ui/core';
import { DoubleArrow } from '@material-ui/icons';
import ConditionallyRender from '../../../../common/ConditionallyRender';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions';
import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
import AccessContext from '../../../../../contexts/AccessContext';
const FeatureStrategiesList = () => {
const smallScreen = useMediaQuery('(max-width:700px)');
const { expandedSidebar, setExpandedSidebar } = useContext(
@ -53,7 +52,7 @@ const FeatureStrategiesList = () => {
const iconClasses = classnames(styles.icon, {
[styles.expandedIcon]: expandedSidebar,
});
return (
<section className={classes}>
<ConditionallyRender
@ -65,7 +64,14 @@ const FeatureStrategiesList = () => {
</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}>
<IconButton
className={styles.iconButton}

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ import {
CF_NAME_ID,
CF_TYPE_ID,
} from '../../../../testIds';
import { CREATE_FEATURE } from '../../../AccessProvider/permissions';
import { CREATE_FEATURE } from '../../../providers/AccessProvider/permissions';
import { projectFilterGenerator } from '../../../../utils/project-filter-generator';
import { useHistory } from 'react-router-dom';
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 ConditionallyRender from '../../../../common/ConditionallyRender/ConditionallyRender';
import AccessContext from '../../../../../contexts/AccessContext';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions';
import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
const StrategyCardHeader = ({
name,

View File

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

View File

@ -14,7 +14,7 @@ import { ReactComponent as UnleashLogo } from '../../../assets/img/logo-dark-wit
import { useStyles } from './Header.styles';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
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 { IPermission } from '../../../interfaces/user';
import NavigationMenu from './NavigationMenu/NavigationMenu';

View File

@ -28,7 +28,7 @@ import AdminApi from '../../page/admin/api';
import AdminUsers from '../../page/admin/users';
import AdminInvoice from '../../page/admin/invoice';
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 NewUser from '../user/NewUser';
import ResetPassword from '../user/ResetPassword/ResetPassword';
@ -40,7 +40,7 @@ import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
import FeatureView2 from '../feature/FeatureView2/FeatureView2';
import FeatureCreate from '../feature/FeatureCreate/FeatureCreate'
import FeatureCreate from '../feature/FeatureCreate/FeatureCreate';
export const routes = [
// Project
@ -164,7 +164,7 @@ export const routes = [
layout: 'main',
menu: { mobile: true },
},
// Applications
{
path: '/applications/:name',
@ -305,8 +305,8 @@ export const routes = [
menu: {},
},
// Addons
{
// Addons
{
path: '/addons/create/:provider',
parent: '/addons',
title: 'Create',

View File

@ -14,7 +14,7 @@ import PageContent from '../../../common/PageContent';
import ResponsiveButton from '../../../common/ResponsiveButton/ResponsiveButton';
import FeatureToggleListNew from '../../../feature/FeatureToggleListNew/FeatureToggleListNew';
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';
interface IProjectFeatureToggles {
@ -61,7 +61,10 @@ const ProjectFeatureToggles = ({
<ResponsiveButton
onClick={() =>
history.push(
getCreateTogglePath(id, uiConfig.flags.E)
getCreateTogglePath(
id,
uiConfig.flags.E
)
)
}
maxWidth="700px"

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { FC } from 'react';
import AccessContext from '../../contexts/AccessContext';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from './permissions';
// 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 {
CREATE_STRATEGY,
DELETE_STRATEGY,
} from '../../AccessProvider/permissions';
} from '../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import PageContent from '../../common/PageContent/PageContent';

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyR
import {
CREATE_TAG_TYPE,
DELETE_TAG_TYPE,
} from '../../AccessProvider/permissions';
} from '../../providers/AccessProvider/permissions';
import Dialogue from '../../common/Dialogue/Dialogue';
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 renderer from 'react-test-renderer';
import theme from '../../../themes/main-theme';
import AccessProvider from '../../AccessProvider/AccessProvider';
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import { createFakeStore } from '../../../accessStoreFake';
import {
CREATE_TAG_TYPE,
UPDATE_TAG_TYPE,
} from '../../AccessProvider/permissions';
} from '../../providers/AccessProvider/permissions';
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 theme from '../../../themes/main-theme';
import { createFakeStore } from '../../../accessStoreFake';
import AccessProvider from '../../AccessProvider/AccessProvider';
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import {
ADMIN,
CREATE_TAG_TYPE,
UPDATE_TAG_TYPE,
DELETE_TAG_TYPE,
} from '../../AccessProvider/permissions';
} from '../../providers/AccessProvider/permissions';
test('renders an empty list correctly', () => {
const tree = renderer.create(

View File

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

View File

@ -14,7 +14,10 @@ import {
} from '@material-ui/core';
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 HeaderTitle from '../../common/HeaderTitle';
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 AuthenticationComponent from './authentication-component';
import AuthenticationComponent from './Authentication';
import {
insecureLogin,
passwordLogin,
demoLogin,
} from '../../store/user/actions';
} from '../../../store/user/actions';
const mapDispatchToProps = (dispatch, props) => ({
demoLogin: (path, user) => demoLogin(path, user)(dispatch),
@ -12,12 +12,4 @@ const mapDispatchToProps = (dispatch, props) => ({
passwordLogin: (path, user) => passwordLogin(path, user)(dispatch),
});
const mapStateToProps = state => ({
user: state.user.toJS(),
flags: state.uiConfig.toJS().flags,
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(AuthenticationComponent);
export default connect(null, mapDispatchToProps)(AuthenticationComponent);

View File

@ -79,50 +79,58 @@ const HostedAuth = ({ authDetails, passwordLogin }) => {
}
/>
<form onSubmit={handleSubmit} action={authDetails.path}>
<Typography variant="subtitle2" className={styles.apiError}>
{apiError}
</Typography>
<div
className={classnames(
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}
<ConditionallyRender
condition={!authDetails.disableDefault}
show={
<form onSubmit={handleSubmit} action={authDetails.path}>
<Typography
variant="subtitle2"
className={styles.apiError}
>
Sign in
</Button>
</Grid>
</div>
</form>
{apiError}
</Typography>
<div
className={classnames(
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 AuthenticationContainer from '../authentication-container';
import AuthenticationContainer from '../Authentication';
import ConditionallyRender from '../../common/ConditionallyRender';
import { useStyles } from './Login.styles';
@ -8,22 +8,21 @@ import useQueryParams from '../../../hooks/useQueryParams';
import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess';
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
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 { permissions, authDetails } = useUser();
const query = useQueryParams();
const history = useHistory();
useEffect(() => {
fetchUser();
/* eslint-disable-next-line */
}, []);
useEffect(() => {
if (user.permissions.length > 0) {
if (permissions?.length > 0) {
history.push('features');
}
/* eslint-disable-next-line */
}, [user.permissions]);
}, [permissions.length]);
const resetPassword = query.get('reset') === 'true';
@ -31,7 +30,7 @@ const Login = ({ history, user, fetchUser }) => {
<StandaloneLayout>
<div className={styles.loginFormContainer}>
<ConditionallyRender
condition={user?.authDetails?.type !== DEMO_TYPE}
condition={authDetails?.type !== DEMO_TYPE}
show={
<h2 className={styles.title}>
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;
return (
<form onSubmit={handleSubmit} action={authDetails.path}>
<ConditionallyRender
condition={apiError}
show={
<Alert severity="error" className={styles.apiError}>
{apiError}
</Alert>
}
/>
<ConditionallyRender
condition={!authDetails.disableDefault}
show={
<form onSubmit={handleSubmit} action={authDetails.path}>
<ConditionallyRender
condition={apiError}
show={
<Alert
severity="error"
className={styles.apiError}
>
{apiError}
</Alert>
}
/>
<div
className={classnames(
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"
autoComplete="true"
size="small"
data-test={LOGIN_EMAIL_ID}
/>
<TextField
label="Password"
onChange={evt => setPassword(evt.target.value)}
name="password"
type="password"
value={password}
error={!!passwordError}
helperText={passwordError}
variant="outlined"
autoComplete="true"
size="small"
data-test={LOGIN_PASSWORD_ID}
/>
<Button
variant="contained"
color="primary"
type="submit"
style={{ width: '150px', margin: '1rem auto' }}
data-test={LOGIN_BUTTON}
>
Sign in
</Button>
</div>
</form>
<div
className={classnames(
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"
autoComplete="true"
size="small"
data-test={LOGIN_EMAIL_ID}
/>
<TextField
label="Password"
onChange={evt => setPassword(evt.target.value)}
name="password"
type="password"
value={password}
error={!!passwordError}
helperText={passwordError}
variant="outlined"
autoComplete="true"
size="small"
data-test={LOGIN_PASSWORD_ID}
/>
<Button
variant="contained"
color="primary"
type="submit"
style={{ width: '150px', margin: '1rem auto' }}
data-test={LOGIN_BUTTON}
>
Sign in
</Button>
</div>
</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) => {
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 {
// @ts-ignore
@ -16,6 +18,6 @@ const handleErrorResponses = (target: string) => async (res: Response) => {
throw error;
}
return res;
}
};
export default handleErrorResponses;

View File

@ -4,20 +4,23 @@ import { formatApiPath } from '../../../../utils/format-path';
import { IPermission } from '../../../../interfaces/user';
import handleErrorResponses from '../httpErrorResponseHandler';
export const USER_CACHE_KEY = `api/admin/user`;
const useUser = () => {
const KEY = `api/admin/user`;
const fetcher = () => {
const path = formatApiPath(`api/admin/user`);
return fetch(path, {
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 refetch = () => {
mutate(KEY);
mutate(USER_CACHE_KEY);
};
useEffect(() => {
@ -28,6 +31,7 @@ const useUser = () => {
user: data?.user || {},
permissions: (data?.permissions || []) as IPermission[],
feedback: data?.feedback || [],
authDetails: data || {},
error,
loading,
refetch,

View File

@ -15,11 +15,10 @@ import { StylesProvider } from '@material-ui/core/styles';
import mainTheme from './themes/main-theme';
import store from './store';
import MetricsPoller from './metrics-poller';
import App from './component/AppContainer';
import ScrollToTop from './component/scroll-to-top';
import { writeWarning } from './security-logger';
import AccessProvider from './component/AccessProvider/AccessProvider';
import AccessProvider from './component/providers/AccessProvider/AccessProvider';
import { getBasePath } from './utils/format-path';
let composeEnhancers;
@ -38,8 +37,6 @@ const unleashStore = createStore(
store,
composeEnhancers(applyMiddleware(thunkMiddleware))
);
const metricsPoller = new MetricsPoller(unleashStore);
metricsPoller.start();
ReactDOM.render(
<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 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 PageContent from '../../../component/common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/AccessProvider/permissions';
import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
const initialState = {
enabled: false,
@ -93,12 +99,14 @@ function GoogleAuth({
</Grid>
<Grid item xs={6} style={{ padding: '20px' }}>
<FormControlLabel
control={ <Switch
onChange={updateEnabled}
value={data.enabled}
name="enabled"
checked={data.enabled}
/>}
control={
<Switch
onChange={updateEnabled}
value={data.enabled}
name="enabled"
checked={data.enabled}
/>
}
label={data.enabled ? 'Enabled' : 'Disabled'}
/>
</Grid>

View File

@ -1,10 +1,16 @@
import React, { useState, useEffect, useContext } from 'react';
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 PageContent from '../../../component/common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import { ADMIN } from '../../../component/AccessProvider/permissions';
import { ADMIN } from '../../../component/providers/AccessProvider/permissions';
import AutoCreateForm from './AutoCreateForm/AutoCreateForm';
const initialState = {
@ -57,7 +63,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
...data,
[field]: value,
});
}
};
const onSubmit = async e => {
e.preventDefault();
@ -100,12 +106,14 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
</Grid>
<Grid item md={6} style={{ padding: '20px' }}>
<FormControlLabel
control={ <Switch
onChange={updateEnabled}
value={data.enabled}
name="enabled"
checked={data.enabled}
/>}
control={
<Switch
onChange={updateEnabled}
value={data.enabled}
name="enabled"
checked={data.enabled}
/>
}
label={data.enabled ? 'Enabled' : 'Disabled'}
/>
</Grid>
@ -125,7 +133,6 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
style={{ width: '400px' }}
variant="outlined"
size="small"
/>
</Grid>
</Grid>
@ -151,7 +158,9 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
<Grid container spacing={3}>
<Grid item md={5}>
<strong>Client secret</strong>
<p>(Required) Client secret of your OpenID application. </p>
<p>
(Required) Client secret of your OpenID application.{' '}
</p>
</Grid>
<Grid item md={6}>
<TextField
@ -171,18 +180,27 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
<Grid container spacing={3}>
<Grid item md={5}>
<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 item md={6} style={{ padding: '20px' }}>
<FormControlLabel
control={ <Switch
onChange={updateSingleSignOut}
value={data.enableSingleSignOut}
disabled={!data.enabled}
name="enableSingleSignOut"
checked={data.enableSingleSignOut}
/>}
label={data.enableSingleSignOut ? 'Enabled' : 'Disabled'}
control={
<Switch
onChange={updateSingleSignOut}
value={data.enableSingleSignOut}
disabled={!data.enabled}
name="enableSingleSignOut"
checked={data.enableSingleSignOut}
/>
}
label={
data.enableSingleSignOut
? 'Enabled'
: 'Disabled'
}
/>
</Grid>
</Grid>
@ -199,7 +217,7 @@ function OidcAuth({ config, getOidcConfig, updateOidcConfig, unleashUrl }) {
Save
</Button>{' '}
<small>{info}</small>
<small style={{color: 'red'}}>{error}</small>
<small style={{ color: 'red' }}>{error}</small>
</Grid>
</Grid>
</form>

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import {
} from '@material-ui/core';
import { Edit, Lock, Delete } from '@material-ui/icons';
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 { formatDateWithLocale } from '../../../../../component/common/util';
import AccessContext from '../../../../../contexts/AccessContext';

View File

@ -14,7 +14,7 @@ import UpdateUser from '../update-user-component';
import DelUser from '../del-user-component';
import ConditionallyRender from '../../../../component/common/ConditionallyRender/ConditionallyRender';
import AccessContext from '../../../../contexts/AccessContext';
import { ADMIN } from '../../../../component/AccessProvider/permissions';
import { ADMIN } from '../../../../component/providers/AccessProvider/permissions';
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
import useUsers from '../../../../hooks/api/getters/useUsers/useUsers';
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 AccessContext from '../../../contexts/AccessContext';
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 HeaderTitle from '../../../component/common/HeaderTitle';
import { Button } from '@material-ui/core';

View File

@ -1,6 +1,6 @@
import { Alert } from '@material-ui/lab';
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 HistoryComponent from '../../component/history/EventHistory';
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';
type objectIdx = {
@ -24,6 +24,6 @@ export const projectFilterGenerator = (
{}
);
return (projectId: string) => {
return admin || permissionMap[projectId]
return admin || permissionMap[projectId];
};
};