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:
parent
ea2086a7f4
commit
005daa3740
@ -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();
|
||||
});
|
||||
});
|
@ -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.
|
||||
|
@ -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 }) => {
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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}>
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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}
|
||||
</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}
|
||||
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} 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
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -31,7 +31,6 @@ const FeatureOverviewEnvironment = ({
|
||||
return strategies.map(strategy => {
|
||||
return (
|
||||
<FeatureOverviewStrategyCard
|
||||
data-loading
|
||||
strategy={strategy}
|
||||
key={strategy.id}
|
||||
onClick={handleClick}
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -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 />
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
The feature <strong>{featureId.substring(0, 30)}</strong>{' '}
|
||||
does not exist. Do you want to
|
||||
<Link to={getCreateTogglePath(projectId)}>create it</Link>
|
||||
?
|
||||
</p>
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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',
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
|
@ -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
|
67
frontend/src/component/providers/SWRProvider/SWRProvider.tsx
Normal file
67
frontend/src/component/providers/SWRProvider/SWRProvider.tsx
Normal 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;
|
@ -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';
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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 = ({
|
||||
|
@ -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';
|
||||
|
102
frontend/src/component/user/Authentication/Authentication.tsx
Normal file
102
frontend/src/component/user/Authentication/Authentication.tsx
Normal 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;
|
@ -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);
|
@ -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>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
@ -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);
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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];
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user