1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +02:00

Merge branch 'main' into fix/api-token-copy

This commit is contained in:
Youssef Khedher 2022-02-15 13:04:14 +01:00 committed by GitHub
commit def7dbf963
52 changed files with 189 additions and 215 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "unleash-frontend", "name": "unleash-frontend",
"description": "unleash your features", "description": "unleash your features",
"version": "4.8.0-beta.1", "version": "4.8.0-beta.5",
"keywords": [ "keywords": [
"unleash", "unleash",
"feature toggle", "feature toggle",
@ -46,7 +46,7 @@
"@types/debounce": "1.2.1", "@types/debounce": "1.2.1",
"@types/deep-diff": "1.0.1", "@types/deep-diff": "1.0.1",
"@types/jest": "27.4.0", "@types/jest": "27.4.0",
"@types/node": "14.18.10", "@types/node": "14.18.12",
"@types/react": "17.0.39", "@types/react": "17.0.39",
"@types/react-dom": "17.0.11", "@types/react-dom": "17.0.11",
"@types/react-outside-click-handler": "1.3.1", "@types/react-outside-click-handler": "1.3.1",
@ -63,7 +63,7 @@
"debounce": "1.2.1", "debounce": "1.2.1",
"deep-diff": "1.0.2", "deep-diff": "1.0.2",
"fast-json-patch": "3.1.0", "fast-json-patch": "3.1.0",
"http-proxy-middleware": "2.0.2", "http-proxy-middleware": "2.0.3",
"@types/lodash.clonedeep": "4.5.6", "@types/lodash.clonedeep": "4.5.6",
"lodash.clonedeep": "4.5.0", "lodash.clonedeep": "4.5.0",
"lodash.flow": "3.5.0", "lodash.flow": "3.5.0",

View File

@ -81,7 +81,7 @@ export const App = () => {
<EnvironmentSplash onFinish={refetchSplash} /> <EnvironmentSplash onFinish={refetchSplash} />
} }
elseShow={ elseShow={
<LayoutPicker location={location}> <LayoutPicker>
<Switch> <Switch>
<ProtectedRoute <ProtectedRoute
exact exact

View File

@ -5,13 +5,13 @@ import {
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemText, ListItemText,
} from '@material-ui/core'; } from '@material-ui/core';
import { Visibility, VisibilityOff, Delete } from '@material-ui/icons'; import { Delete, Edit, Visibility, VisibilityOff } from '@material-ui/icons';
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
import { import {
DELETE_ADDON, DELETE_ADDON,
UPDATE_ADDON, UPDATE_ADDON,
} from '../../../providers/AccessProvider/permissions'; } from '../../../providers/AccessProvider/permissions';
import { Link } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import PageContent from '../../../common/PageContent/PageContent'; import PageContent from '../../../common/PageContent/PageContent';
import useAddons from '../../../../hooks/api/getters/useAddons/useAddons'; import useAddons from '../../../../hooks/api/getters/useAddons/useAddons';
import useToast from '../../../../hooks/useToast'; import useToast from '../../../../hooks/useToast';
@ -31,6 +31,7 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
const { updateAddon, removeAddon } = useAddonsApi(); const { updateAddon, removeAddon } = useAddonsApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const history = useHistory();
const [showDelete, setShowDelete] = useState(false); const [showDelete, setShowDelete] = useState(false);
const [deletedAddon, setDeletedAddon] = useState<IAddon>({ const [deletedAddon, setDeletedAddon] = useState<IAddon>({
id: 0, id: 0,
@ -115,10 +116,19 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
> >
<ConditionallyRender <ConditionallyRender
condition={addon.enabled} condition={addon.enabled}
show={<Visibility />} show={<Visibility titleAccess="Disable addon" />}
elseShow={<VisibilityOff />} elseShow={<VisibilityOff titleAccess="Enable addon" />}
/> />
</PermissionIconButton> </PermissionIconButton>
<PermissionIconButton
permission={UPDATE_ADDON}
tooltip={'Edit Addon'}
onClick={() => {
history.push(`/addons/edit/${addon.id}`);
}}
>
<Edit titleAccess="Edit Addon" />
</PermissionIconButton>
<PermissionIconButton <PermissionIconButton
permission={DELETE_ADDON} permission={DELETE_ADDON}
tooltip={'Remove Addon'} tooltip={'Remove Addon'}
@ -127,7 +137,7 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
setShowDelete(true); setShowDelete(true);
}} }}
> >
<Delete /> <Delete titleAccess="Remove Addon" />
</PermissionIconButton> </PermissionIconButton>
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItem>

View File

@ -1,27 +1,21 @@
import PropTypes from 'prop-types';
import { ApiTokenList } from '../api-token/ApiTokenList/ApiTokenList'; import { ApiTokenList } from '../api-token/ApiTokenList/ApiTokenList';
import AdminMenu from '../menu/AdminMenu'; import AdminMenu from '../menu/AdminMenu';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import { useContext } from 'react'; import { useContext } from 'react';
const ApiPage = ({ history }) => { const ApiPage = () => {
const { isAdmin } = useContext(AccessContext); const { isAdmin } = useContext(AccessContext);
return ( return (
<div> <div>
<ConditionallyRender <ConditionallyRender
condition={isAdmin} condition={isAdmin}
show={<AdminMenu history={history} />} show={<AdminMenu />}
/> />
<ApiTokenList /> <ApiTokenList />
</div> </div>
); );
}; };
ApiPage.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default ApiPage; export default ApiPage;

View File

@ -18,7 +18,7 @@ const ProjectRoles = () => {
return ( return (
<div> <div>
<AdminMenu history={history} /> <AdminMenu />
<PageContent <PageContent
bodyClass={styles.rolesListBody} bodyClass={styles.rolesListBody}
headerContent={ headerContent={

View File

@ -38,7 +38,6 @@ const EditUser = () => {
} = useAddUserForm( } = useAddUserForm(
user?.name, user?.name,
user?.email, user?.email,
user?.sendEmail,
user?.rootRole user?.rootRole
); );

View File

@ -18,7 +18,7 @@ const UsersAdmin = () => {
return ( return (
<div> <div>
<AdminMenu history={history} /> <AdminMenu />
<PageContent <PageContent
bodyClass={styles.userListBody} bodyClass={styles.userListBody}
headerContent={ headerContent={

View File

@ -114,8 +114,6 @@ const ChangePassword = ({
password={data.password} password={data.password}
callback={setValidPassword} callback={setValidPassword}
/> />
<p style={{ color: 'red' }}>{error.general}</p>
<TextField <TextField
label="New password" label="New password"
name="password" name="password"

View File

@ -15,7 +15,6 @@ import {
FlagRounded, FlagRounded,
SvgIconComponent, SvgIconComponent,
} from '@material-ui/icons'; } from '@material-ui/icons';
import { shorten } from '../../common';
import { import {
CREATE_FEATURE, CREATE_FEATURE,
CREATE_STRATEGY, CREATE_STRATEGY,
@ -87,9 +86,14 @@ export const ApplicationView = () => {
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={ primary={
<Link to={`${viewUrl}/${name}`}>{shorten(name, 50)}</Link> <Link
to={`${viewUrl}/${name}`}
style={{ wordBreak: 'break-all' }}
>
{name}
</Link>
} }
secondary={shorten(description, 60)} secondary={description}
/> />
</ListItem> </ListItem>
); );

View File

@ -18,7 +18,6 @@ test('renders correctly if no application', () => {
fetchApplication={() => Promise.resolve({})} fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()} storeApplicationMetaData={jest.fn()}
deleteApplication={jest.fn()} deleteApplication={jest.fn()}
history={{}}
locationSettings={{ locale: 'en-GB' }} locationSettings={{ locale: 'en-GB' }}
/> />
</MemoryRouter> </MemoryRouter>
@ -42,7 +41,6 @@ test('renders correctly without permission', () => {
fetchApplication={() => Promise.resolve({})} fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()} storeApplicationMetaData={jest.fn()}
deleteApplication={jest.fn()} deleteApplication={jest.fn()}
history={{}}
application={{ application={{
appName: 'test-app', appName: 'test-app',
instances: [ instances: [
@ -104,7 +102,6 @@ test('renders correctly with permissions', () => {
<ApplicationEdit <ApplicationEdit
fetchApplication={() => Promise.resolve({})} fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()} storeApplicationMetaData={jest.fn()}
history={{}}
deleteApplication={jest.fn()} deleteApplication={jest.fn()}
application={{ application={{
appName: 'test-app', appName: 'test-app',

View File

@ -18,7 +18,6 @@ test('renders correctly if no application', () => {
fetchApplication={() => Promise.resolve({})} fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()} storeApplicationMetaData={jest.fn()}
deleteApplication={jest.fn()} deleteApplication={jest.fn()}
history={{}}
locationSettings={{ locale: 'en-GB' }} locationSettings={{ locale: 'en-GB' }}
/> />
</MemoryRouter> </MemoryRouter>
@ -42,7 +41,6 @@ test('renders correctly without permission', () => {
fetchApplication={() => Promise.resolve({})} fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()} storeApplicationMetaData={jest.fn()}
deleteApplication={jest.fn()} deleteApplication={jest.fn()}
history={{}}
application={{ application={{
appName: 'test-app', appName: 'test-app',
instances: [ instances: [
@ -104,7 +102,6 @@ test('renders correctly with permissions', () => {
<ApplicationEdit <ApplicationEdit
fetchApplication={() => Promise.resolve({})} fetchApplication={() => Promise.resolve({})}
storeApplicationMetaData={jest.fn()} storeApplicationMetaData={jest.fn()}
history={{}}
deleteApplication={jest.fn()} deleteApplication={jest.fn()}
application={{ application={{
appName: 'test-app', appName: 'test-app',

View File

@ -7,7 +7,7 @@ import useToast from '../../hooks/useToast';
import { useFeaturesSort } from '../../hooks/useFeaturesSort'; import { useFeaturesSort } from '../../hooks/useFeaturesSort';
export const ArchiveListContainer = () => { export const ArchiveListContainer = () => {
const { setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { reviveFeature } = useFeatureArchiveApi(); const { reviveFeature } = useFeatureArchiveApi();
const { archivedFeatures, loading, refetchArchived } = useFeaturesArchive(); const { archivedFeatures, loading, refetchArchived } = useFeaturesArchive();
@ -17,6 +17,14 @@ export const ArchiveListContainer = () => {
const revive = (feature: string) => { const revive = (feature: string) => {
reviveFeature(feature) reviveFeature(feature)
.then(refetchArchived) .then(refetchArchived)
.then(() =>
setToastData({
type: 'success',
title: "And we're back!",
text: 'The feature toggle has been revived.',
confetti: true,
})
)
.catch(e => setToastApiError(e.toString())); .catch(e => setToastApiError(e.toString()));
}; };

View File

@ -5,8 +5,16 @@ export const useStyles = makeStyles(theme => ({
position: 'absolute', position: 'absolute',
top: '4px', top: '4px',
}, },
breadcrumbNavParagraph: { color: 'inherit' }, breadcrumbNavParagraph: {
color: 'inherit',
'& > *': {
verticalAlign: 'middle',
},
},
breadcrumbLink: { breadcrumbLink: {
textDecoration: 'none', textDecoration: 'none',
'& > *': {
verticalAlign: 'middle',
},
}, },
})); }));

View File

@ -4,6 +4,7 @@ import ConditionallyRender from '../ConditionallyRender';
import { useStyles } from './BreadcrumbNav.styles'; import { useStyles } from './BreadcrumbNav.styles';
import AccessContext from '../../../contexts/AccessContext'; import AccessContext from '../../../contexts/AccessContext';
import { useContext } from 'react'; import { useContext } from 'react';
import StringTruncator from '../StringTruncator/StringTruncator';
const BreadcrumbNav = () => { const BreadcrumbNav = () => {
const { isAdmin } = useContext(AccessContext); const { isAdmin } = useContext(AccessContext);
@ -51,7 +52,7 @@ const BreadcrumbNav = () => {
styles.breadcrumbNavParagraph styles.breadcrumbNavParagraph
} }
> >
{path.substring(0, 30)} <StringTruncator text={path} maxWidth="200" />
</p> </p>
); );
} }
@ -72,7 +73,7 @@ const BreadcrumbNav = () => {
className={styles.breadcrumbLink} className={styles.breadcrumbLink}
to={link} to={link}
> >
{path.substring(0, 30)} <StringTruncator text={path} maxWidth="200" />
</Link> </Link>
); );
})} })}

View File

@ -9,7 +9,7 @@ interface IPermissionIconButtonProps
> { > {
permission: string; permission: string;
Icon?: React.ElementType; Icon?: React.ElementType;
tooltip: string; tooltip?: string;
onClick?: (e: any) => void; onClick?: (e: any) => void;
projectId?: string; projectId?: string;
environmentId?: string; environmentId?: string;

View File

@ -72,8 +72,6 @@ const ProjectSelect = ({ currentProjectId, updateCurrentProject, ...rest }) => {
}; };
ProjectSelect.propTypes = { ProjectSelect.propTypes = {
projects: PropTypes.array.isRequired,
fetchProjects: PropTypes.func.isRequired,
currentProjectId: PropTypes.string.isRequired, currentProjectId: PropTypes.string.isRequired,
updateCurrentProject: PropTypes.func.isRequired, updateCurrentProject: PropTypes.func.isRequired,
}; };

View File

@ -38,6 +38,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
permission={permission} permission={permission}
projectId={projectId} projectId={projectId}
environmentId={environmentId} environmentId={environmentId}
tooltip={tooltip}
data-loading data-loading
{...rest} {...rest}
> >
@ -53,6 +54,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
variant="contained" variant="contained"
disabled={disabled} disabled={disabled}
environmentId={environmentId} environmentId={environmentId}
tooltip={tooltip}
data-loading data-loading
{...rest} {...rest}
> >

View File

@ -19,8 +19,6 @@ import ConditionallyRender from './ConditionallyRender/ConditionallyRender';
export { styles }; export { styles };
export const shorten = (str, len = 50) =>
str && str.length > len ? `${str.substring(0, len)}...` : str;
export const AppsLinkList = ({ apps }) => ( export const AppsLinkList = ({ apps }) => (
<List> <List>
<ConditionallyRender <ConditionallyRender

View File

@ -112,11 +112,11 @@ const ContextForm: React.FC<IContextForm> = ({
autoFocus autoFocus
/> />
<p className={styles.inputDescription}> <p className={styles.inputDescription}>
What is this context for? What is this context for?
</p> </p>
<TextField <TextField
className={styles.input} className={styles.input}
label="Context description" label="Context description (optional)"
variant="outlined" variant="outlined"
multiline multiline
maxRows={4} maxRows={4}
@ -139,7 +139,7 @@ const ContextForm: React.FC<IContextForm> = ({
})} })}
<div className={styles.tagContainer}> <div className={styles.tagContainer}>
<TextField <TextField
label="Value" label="Value (optional)"
name="value" name="value"
className={styles.tagInput} className={styles.tagInput}
value={value} value={value}

View File

@ -159,7 +159,6 @@ const EnvironmentListItem = ({
<Tooltip title={`${tooltipText} environment`}> <Tooltip title={`${tooltipText} environment`}>
<IconButton <IconButton
aria-label="disable" aria-label="disable"
disabled={env.protected}
onClick={() => { onClick={() => {
setSelectedEnv(env); setSelectedEnv(env);
setToggleDialog(prev => !prev); setToggleDialog(prev => !prev);

View File

@ -55,7 +55,6 @@ const EditFeature = () => {
history.push(`/projects/${project}/features/${name}`); history.push(`/projects/${project}/features/${name}`);
setToastData({ setToastData({
title: 'Toggle updated successfully', title: 'Toggle updated successfully',
text: 'Now you can start using your toggle.',
type: 'success', type: 'success',
}); });
} catch (e: any) { } catch (e: any) {

View File

@ -5,6 +5,9 @@ import { DialogContentText } from '@material-ui/core';
import ConditionallyRender from '../../../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../../common/ConditionallyRender/ConditionallyRender';
import Dialogue from '../../../../common/Dialogue'; import Dialogue from '../../../../common/Dialogue';
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature'; import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
import React from 'react';
import useToast from '../../../../../hooks/useToast';
import { formatUnknownError } from '../../../../../utils/format-unknown-error';
interface IStaleDialogProps { interface IStaleDialogProps {
open: boolean; open: boolean;
@ -13,6 +16,7 @@ interface IStaleDialogProps {
} }
const StaleDialog = ({ open, setOpen, stale }: IStaleDialogProps) => { const StaleDialog = ({ open, setOpen, stale }: IStaleDialogProps) => {
const { setToastData, setToastApiError } = useToast();
const { projectId, featureId } = useParams<IFeatureViewParams>(); const { projectId, featureId } = useParams<IFeatureViewParams>();
const { patchFeatureToggle } = useFeatureApi(); const { patchFeatureToggle } = useFeatureApi();
const { refetch } = useFeature(projectId, featureId); const { refetch } = useFeature(projectId, featureId);
@ -30,12 +34,31 @@ const StaleDialog = ({ open, setOpen, stale }: IStaleDialogProps) => {
const toggleActionText = stale ? 'active' : 'stale'; const toggleActionText = stale ? 'active' : 'stale';
const onSubmit = async e => { const onSubmit = async (event: React.SyntheticEvent) => {
e.stopPropagation(); event.stopPropagation();
const patch = [{ op: 'replace', path: '/stale', value: !stale }];
await patchFeatureToggle(projectId, featureId, patch); try {
refetch(); const patch = [{ op: 'replace', path: '/stale', value: !stale }];
setOpen(false); await patchFeatureToggle(projectId, featureId, patch);
refetch();
setOpen(false);
} catch (err: unknown) {
setToastApiError(formatUnknownError(err));
}
if (stale) {
setToastData({
type: 'success',
title: "And we're back!",
text: 'The toggle is no longer marked as stale.',
});
} else {
setToastData({
type: 'success',
title: 'A job well done.',
text: 'The toggle has been marked as stale.',
});
}
}; };
const onCancel = () => { const onCancel = () => {

View File

@ -44,4 +44,7 @@ export const useStyles = makeStyles(theme => ({
flexDirection: 'column', flexDirection: 'column',
}, },
}, },
featureId: {
wordBreak: 'break-all'
}
})); }));

View File

@ -1,6 +1,6 @@
import { Tab, Tabs, useMediaQuery } from '@material-ui/core'; import { Tab, Tabs, useMediaQuery } from '@material-ui/core';
import { useState } from 'react'; import { useState } from 'react';
import { WatchLater, Archive, FileCopy, Label } from '@material-ui/icons'; import { Archive, FileCopy, Label, WatchLater } from '@material-ui/icons';
import { Link, Route, useHistory, useParams } from 'react-router-dom'; import { Link, Route, useHistory, useParams } from 'react-router-dom';
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi'; import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
import useFeature from '../../../hooks/api/getters/useFeature/useFeature'; import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
@ -115,7 +115,8 @@ const FeatureView = () => {
return ( return (
<div> <div>
<p> <p>
The feature <strong>{featureId.substring(0, 30)}</strong>{' '} The feature{' '}
<strong className={styles.featureId}>{featureId}</strong>{' '}
does not exist. Do you want to &nbsp; does not exist. Do you want to &nbsp;
<Link <Link
to={getCreateTogglePath(projectId, uiConfig.flags.E, { to={getCreateTogglePath(projectId, uiConfig.flags.E, {

View File

@ -29,9 +29,10 @@ const useFeatureForm = (
}, [initialType]); }, [initialType]);
useEffect(() => { useEffect(() => {
if (!toggleQueryName) setName(initialName); if (!name) {
else setName(toggleQueryName); setName(toggleQueryName || initialName);
}, [initialName, toggleQueryName]); }
}, [name, initialName, toggleQueryName]);
useEffect(() => { useEffect(() => {
if (!projectId) setProject(initialProject); if (!projectId) setProject(initialProject);

View File

@ -1,11 +1,11 @@
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { ADMIN } from '../../component/providers/AccessProvider/permissions'; import { ADMIN } from '../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../component/common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import { EventHistory } from '../../component/history/EventHistory/EventHistory'; import AccessContext from '../../../contexts/AccessContext';
import AccessContext from '../../contexts/AccessContext'; import { EventHistory } from '../EventHistory/EventHistory';
const HistoryPage = () => { export const EventHistoryPage = () => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
return ( return (
@ -20,5 +20,3 @@ const HistoryPage = () => {
/> />
); );
}; };
export default HistoryPage;

View File

@ -0,0 +1,9 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { FeatureEventHistory } from '../FeatureEventHistory/FeatureEventHistory';
export const FeatureEventHistoryPage = () => {
const { toggleName } = useParams<{ toggleName: string }>();
return <FeatureEventHistory toggleName={toggleName} />;
};

View File

@ -1,8 +1,10 @@
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import { matchPath } from 'react-router'; import { matchPath } from 'react-router';
import { useLocation } from 'react-router-dom';
import { MainLayout } from '../MainLayout/MainLayout'; import { MainLayout } from '../MainLayout/MainLayout';
const LayoutPicker = ({ children, location }) => { const LayoutPicker = ({ children }) => {
const location = useLocation();
const standalonePages = () => { const standalonePages = () => {
const { pathname } = location; const { pathname } = location;
const isLoginPage = matchPath(pathname, { path: '/login' }); const isLoginPage = matchPath(pathname, { path: '/login' });

View File

@ -27,7 +27,7 @@ const Header = () => {
const [anchorEl, setAnchorEl] = useState(); const [anchorEl, setAnchorEl] = useState();
const [anchorElAdvanced, setAnchorElAdvanced] = useState(); const [anchorElAdvanced, setAnchorElAdvanced] = useState();
const [admin, setAdmin] = useState(false); const [admin, setAdmin] = useState(false);
const { permissions } = useAuthPermissions() const { permissions } = useAuthPermissions();
const commonStyles = useCommonStyles(); const commonStyles = useCommonStyles();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const smallScreen = useMediaQuery(theme.breakpoints.down('sm')); const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
@ -68,12 +68,19 @@ const Header = () => {
className={styles.drawerButton} className={styles.drawerButton}
onClick={toggleDrawer} onClick={toggleDrawer}
> >
<MenuIcon /> <MenuIcon titleAccess="Menu" />
</IconButton> </IconButton>
} }
elseShow={ elseShow={
<Link to="/" className={commonStyles.flexRow}> <Link
<UnleashLogo className={styles.logo} />{' '} to="/"
className={commonStyles.flexRow}
aria-label="Home"
>
<UnleashLogo
className={styles.logo}
aria-label="Unleash logo"
/>
</Link> </Link>
} }
/> />
@ -127,6 +134,7 @@ const Header = () => {
> >
<MenuBookIcon <MenuBookIcon
className={styles.docsIcon} className={styles.docsIcon}
titleAccess="Documentation"
/> />
</a> </a>
</Tooltip> </Tooltip>
@ -140,6 +148,7 @@ const Header = () => {
> >
<SettingsIcon <SettingsIcon
className={styles.docsIcon} className={styles.docsIcon}
titleAccess="Settings"
/> />
</IconButton> </IconButton>
} }

View File

@ -1,9 +1,7 @@
import { FeatureToggleListContainer } from '../feature/FeatureToggleList/FeatureToggleListContainer'; import { FeatureToggleListContainer } from '../feature/FeatureToggleList/FeatureToggleListContainer';
import { StrategyForm } from '../strategies/StrategyForm/StrategyForm'; import { StrategyForm } from '../strategies/StrategyForm/StrategyForm';
import { StrategyView } from '../../component/strategies/StrategyView/StrategyView'; import { StrategyView } from '../strategies/StrategyView/StrategyView';
import { StrategiesList } from '../strategies/StrategiesList/StrategiesList'; import { StrategiesList } from '../strategies/StrategiesList/StrategiesList';
import HistoryPage from '../../page/history';
import HistoryTogglePage from '../../page/history/toggle';
import { ArchiveListContainer } from '../archive/ArchiveListContainer'; import { ArchiveListContainer } from '../archive/ArchiveListContainer';
import { TagTypeList } from '../tags/TagTypeList/TagTypeList'; import { TagTypeList } from '../tags/TagTypeList/TagTypeList';
import { AddonList } from '../addons/AddonList/AddonList'; import { AddonList } from '../addons/AddonList/AddonList';
@ -45,6 +43,8 @@ import RedirectFeatureView from '../feature/RedirectFeatureView/RedirectFeatureV
import { CreateAddon } from '../addons/CreateAddon/CreateAddon'; import { CreateAddon } from '../addons/CreateAddon/CreateAddon';
import { EditAddon } from '../addons/EditAddon/EditAddon'; import { EditAddon } from '../addons/EditAddon/EditAddon';
import { CopyFeatureToggle } from '../feature/CopyFeature/CopyFeature'; import { CopyFeatureToggle } from '../feature/CopyFeature/CopyFeature';
import { EventHistoryPage } from '../history/EventHistoryPage/EventHistoryPage';
import { FeatureEventHistoryPage } from '../history/FeatureEventHistoryPage/FeatureEventHistoryPage';
export const routes = [ export const routes = [
// Project // Project
@ -354,7 +354,7 @@ export const routes = [
path: '/history/:toggleName', path: '/history/:toggleName',
title: ':toggleName', title: ':toggleName',
parent: '/history', parent: '/history',
component: HistoryTogglePage, component: FeatureEventHistoryPage,
type: 'protected', type: 'protected',
layout: 'main', layout: 'main',
menu: {}, menu: {},
@ -362,7 +362,7 @@ export const routes = [
{ {
path: '/history', path: '/history',
title: 'Event History', title: 'Event History',
component: HistoryPage, component: EventHistoryPage,
type: 'protected', type: 'protected',
layout: 'main', layout: 'main',
menu: { adminSettings: true }, menu: { adminSettings: true },

View File

@ -32,4 +32,18 @@ export const useStyles = makeStyles(theme => ({
width: 'auto', width: 'auto',
fontSize: '1rem', fontSize: '1rem',
}, },
title: {
fontSize: theme.fontSizes.mainHeader,
fontWeight: 'bold',
marginBottom: '0.5rem',
display: 'grid',
gridTemplateColumns: '1fr auto',
alignItems: 'center',
gridGap: '1rem',
},
titleText: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
})); }));

View File

@ -1,5 +1,4 @@
import { useHistory, useParams } from 'react-router'; import { useHistory, useParams } from 'react-router';
import { useCommonStyles } from '../../../common.styles';
import useProject from '../../../hooks/api/getters/useProject/useProject'; import useProject from '../../../hooks/api/getters/useProject/useProject';
import useLoading from '../../../hooks/useLoading'; import useLoading from '../../../hooks/useLoading';
import ApiError from '../../common/ApiError/ApiError'; import ApiError from '../../common/ApiError/ApiError';
@ -25,7 +24,6 @@ const Project = () => {
const { project, error, loading, refetch } = useProject(id); const { project, error, loading, refetch } = useProject(id);
const ref = useLoading(loading); const ref = useLoading(loading);
const { setToastData } = useToast(); const { setToastData } = useToast();
const commonStyles = useCommonStyles();
const styles = useStyles(); const styles = useStyles();
const history = useHistory(); const history = useHistory();
@ -121,10 +119,10 @@ const Project = () => {
<div className={styles.innerContainer}> <div className={styles.innerContainer}>
<h2 <h2
data-loading data-loading
className={commonStyles.title} className={styles.title}
style={{ margin: 0 }} style={{ margin: 0 }}
> >
Project: {project?.name}{' '} <div className={styles.titleText}>{project?.name}</div>
<PermissionIconButton <PermissionIconButton
permission={UPDATE_PROJECT} permission={UPDATE_PROJECT}
tooltip="Edit" tooltip="Edit"

View File

@ -23,6 +23,11 @@ export const useStyles = makeStyles(theme => ({
title: { title: {
fontWeight: 'normal', fontWeight: 'normal',
fontSize: '1rem', fontSize: '1rem',
lineClamp: 2,
display: '-webkit-box',
boxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}, },
projectIcon: { projectIcon: {

View File

@ -46,7 +46,7 @@ const ProjectCard = ({
return ( return (
<Card className={styles.projectCard} onMouseEnter={onHover}> <Card className={styles.projectCard} onMouseEnter={onHover}>
<div className={styles.header} data-loading> <div className={styles.header} data-loading>
<h2 className={styles.title}>{name}</h2> <div className={styles.title}>{name}</div>
<PermissionIconButton <PermissionIconButton
permission={UPDATE_PROJECT} permission={UPDATE_PROJECT}

View File

@ -11,6 +11,7 @@ import useStrategiesApi from '../../../hooks/api/actions/useStrategiesApi/useStr
import { IStrategy } from '../../../interfaces/strategy'; import { IStrategy } from '../../../interfaces/strategy';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies'; import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies';
import { formatUnknownError } from '../../../utils/format-unknown-error';
interface ICustomStrategyParams { interface ICustomStrategyParams {
name?: string; name?: string;
@ -78,7 +79,7 @@ export const StrategyForm = ({ editMode, strategy }: IStrategyFormProps) => {
setParams(prev => [...parameters]); setParams(prev => [...parameters]);
if (editMode) { if (editMode) {
try { try {
await updateStrategy({ name, description, parameters: params }); await updateStrategy({ name, description, parameters });
history.push(`/strategies/view/${name}`); history.push(`/strategies/view/${name}`);
setToastData({ setToastData({
type: 'success', type: 'success',
@ -86,12 +87,12 @@ export const StrategyForm = ({ editMode, strategy }: IStrategyFormProps) => {
text: 'Successfully updated strategy', text: 'Successfully updated strategy',
}); });
refetchStrategies(); refetchStrategies();
} catch (e: any) { } catch (error: unknown) {
setToastApiError(e.toString()); setToastApiError(formatUnknownError(error));
} }
} else { } else {
try { try {
await createStrategy({ name, description, parameters: params }); await createStrategy({ name, description, parameters });
history.push(`/strategies`); history.push(`/strategies`);
setToastData({ setToastData({
type: 'success', type: 'success',
@ -99,13 +100,8 @@ export const StrategyForm = ({ editMode, strategy }: IStrategyFormProps) => {
text: 'Successfully created new strategy', text: 'Successfully created new strategy',
}); });
refetchStrategies(); refetchStrategies();
} catch (e: any) { } catch (error: unknown) {
const STRATEGY_EXIST_ERROR = 'Error: Strategy with name'; setToastApiError(formatUnknownError(error));
if (e.toString().includes(STRATEGY_EXIST_ERROR)) {
setErrors({
name: 'A strategy with this name already exists',
});
}
} }
} }
}; };

View File

@ -23,7 +23,6 @@ test('renders correctly with one strategy', () => {
removeStrategy={jest.fn()} removeStrategy={jest.fn()}
deprecateStrategy={jest.fn()} deprecateStrategy={jest.fn()}
reactivateStrategy={jest.fn()} reactivateStrategy={jest.fn()}
history={{}}
/> />
</AccessProvider> </AccessProvider>
</UIProvider> </UIProvider>
@ -50,7 +49,6 @@ test('renders correctly with one strategy without permissions', () => {
removeStrategy={jest.fn()} removeStrategy={jest.fn()}
deprecateStrategy={jest.fn()} deprecateStrategy={jest.fn()}
reactivateStrategy={jest.fn()} reactivateStrategy={jest.fn()}
history={{}}
/> />
</AccessProvider> </AccessProvider>
</UIProvider> </UIProvider>

View File

@ -45,7 +45,6 @@ test('renders correctly with one strategy', () => {
fetchStrategies={jest.fn()} fetchStrategies={jest.fn()}
fetchApplications={jest.fn()} fetchApplications={jest.fn()}
fetchFeatureToggles={jest.fn()} fetchFeatureToggles={jest.fn()}
history={{}}
/> />
</ThemeProvider> </ThemeProvider>
</AccessProvider> </AccessProvider>

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
import { formatUnknownError } from '../../../utils/format-unknown-error';
const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => { const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => {
const [tagName, setTagName] = useState(initialTagName); const [tagName, setTagName] = useState(initialTagName);
@ -22,8 +23,6 @@ const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => {
}; };
}; };
const NAME_EXISTS_ERROR =
'There already exists a tag-type with the name simple';
const validateNameUniqueness = async () => { const validateNameUniqueness = async () => {
if (tagName.length === 0) { if (tagName.length === 0) {
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' })); setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
@ -39,14 +38,12 @@ const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => {
try { try {
await validateTagName(tagName); await validateTagName(tagName);
return true; return true;
} catch (e: any) { } catch (err: unknown) {
if (e.toString().includes(NAME_EXISTS_ERROR)) { setErrors(prev => ({
setErrors(prev => ({ ...prev,
...prev, name: formatUnknownError(err)
name: NAME_EXISTS_ERROR, }));
})); return false;
return false;
}
} }
}; };

View File

@ -22,7 +22,6 @@ test('renders an empty list correctly', () => {
tagTypes={[]} tagTypes={[]}
fetchTagTypes={jest.fn()} fetchTagTypes={jest.fn()}
removeTagType={jest.fn()} removeTagType={jest.fn()}
history={{}}
/> />
</AccessProvider> </AccessProvider>
</UIProvider> </UIProvider>
@ -53,7 +52,6 @@ test('renders a list with elements correctly', () => {
]} ]}
fetchTagTypes={jest.fn()} fetchTagTypes={jest.fn()}
removeTagType={jest.fn()} removeTagType={jest.fn()}
history={{}}
/> />
</AccessProvider> </AccessProvider>
</UIProvider> </UIProvider>

View File

@ -10,8 +10,13 @@ import AuthOptions from '../common/AuthOptions/AuthOptions';
import DividerText from '../../common/DividerText/DividerText'; import DividerText from '../../common/DividerText/DividerText';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import PasswordField from '../../common/PasswordField/PasswordField'; import PasswordField from '../../common/PasswordField/PasswordField';
import { useAuthApi } from "../../../hooks/api/actions/useAuthApi/useAuthApi"; import { useAuthApi } from '../../../hooks/api/actions/useAuthApi/useAuthApi';
import { useAuthUser } from '../../../hooks/api/getters/useAuth/useAuthUser'; import { useAuthUser } from '../../../hooks/api/getters/useAuth/useAuthUser';
import {
LOGIN_BUTTON,
LOGIN_EMAIL_ID,
LOGIN_PASSWORD_ID,
} from '../../../testIds';
const HostedAuth = ({ authDetails }) => { const HostedAuth = ({ authDetails }) => {
const commonStyles = useCommonStyles(); const commonStyles = useCommonStyles();
@ -19,7 +24,7 @@ const HostedAuth = ({ authDetails }) => {
const { refetchUser } = useAuthUser(); const { refetchUser } = useAuthUser();
const history = useHistory(); const history = useHistory();
const params = useQueryParams(); const params = useQueryParams();
const { passwordAuth } = useAuthApi() const { passwordAuth } = useAuthApi();
const [username, setUsername] = useState(params.get('email') || ''); const [username, setUsername] = useState(params.get('email') || '');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [errors, setErrors] = useState({ const [errors, setErrors] = useState({
@ -108,6 +113,7 @@ const HostedAuth = ({ authDetails }) => {
helperText={usernameError} helperText={usernameError}
variant="outlined" variant="outlined"
size="small" size="small"
data-test={LOGIN_EMAIL_ID}
/> />
<PasswordField <PasswordField
label="Password" label="Password"
@ -116,6 +122,7 @@ const HostedAuth = ({ authDetails }) => {
value={password} value={password}
error={!!passwordError} error={!!passwordError}
helperText={passwordError} helperText={passwordError}
data-test={LOGIN_PASSWORD_ID}
/> />
<Grid container> <Grid container>
<Button <Button
@ -123,6 +130,7 @@ const HostedAuth = ({ authDetails }) => {
color="primary" color="primary"
type="submit" type="submit"
className={styles.button} className={styles.button}
data-test={LOGIN_BUTTON}
> >
Sign in Sign in
</Button> </Button>

View File

@ -14,10 +14,11 @@ export const handleBadRequest = async (
if (!setErrors || !requestId) return; if (!setErrors || !requestId) return;
if (res) { if (res) {
const data = await res.json(); const data = await res.json();
const message = data.isJoi ? data.details[0].message : data[0].msg;
setErrors(prev => ({ setErrors(prev => ({
...prev, ...prev,
[requestId]: data[0].msg, [requestId]: message,
})); }));
} }
@ -47,10 +48,11 @@ export const handleUnauthorized = async (
if (!setErrors || !requestId) return; if (!setErrors || !requestId) return;
if (res) { if (res) {
const data = await res.json(); const data = await res.json();
const message = data.isJoi ? data.details[0].message : data[0].msg;
setErrors(prev => ({ setErrors(prev => ({
...prev, ...prev,
[requestId]: data[0].msg, [requestId]: message,
})); }));
} }
@ -65,7 +67,6 @@ export const handleForbidden = async (
if (!setErrors || !requestId) return; if (!setErrors || !requestId) return;
if (res) { if (res) {
const data = await res.json(); const data = await res.json();
const message = data.isJoi ? data.details[0].message : data[0].msg; const message = data.isJoi ? data.details[0].message : data[0].msg;
setErrors(prev => ({ setErrors(prev => ({

View File

@ -58,7 +58,3 @@ export const useAuthApi = (): IUseAuthApiOutput => {
loading, loading,
}; };
}; };
const ensureRelativePath = (path: string): string => {
return path.replace(/^\//, '');
};

View File

@ -1,6 +0,0 @@
import React from 'react';
import ClientInstance from '../../component/client-instance/client-instance-container';
const render = () => <ClientInstance />;
export default render;

View File

@ -1,13 +0,0 @@
import React from 'react';
import CreateFeature from '../../component/feature/create/CreateFeature';
import PropTypes from 'prop-types';
const render = ({ history }) => (
<CreateFeature title="Create feature toggle" history={history} />
);
render.propTypes = {
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -1,13 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FeatureEventHistory } from '../../component/history/FeatureEventHistory/FeatureEventHistory';
const render = ({ match: { params } }) => (
<FeatureEventHistory toggleName={params.toggleName} />
);
render.propTypes = {
match: PropTypes.object.isRequired,
};
export default render;

View File

@ -1,6 +0,0 @@
import React from 'react';
import Metrics from '../../component/metrics/metrics-container';
const render = () => <Metrics />;
export default render;

View File

@ -1,11 +0,0 @@
import React from 'react';
import CreateProject from '../../component/project/create-project-container';
import PropTypes from 'prop-types';
const render = ({ history }) => <CreateProject title="Create Project" history={history} />;
render.propTypes = {
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -1,14 +0,0 @@
import React from 'react';
import EditProject from '../../component/project/edit-project-container';
import PropTypes from 'prop-types';
const render = ({ match: { params }, history }) => (
<EditProject projectId={params.id} title="Edit project" history={history} />
);
render.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -1,6 +0,0 @@
import React from 'react';
import ProjectEnvironment from '../../component/project/ProjectEnvironment/ProjectEnvironment';
const ProjectEnvironmentConfigPage = () => <ProjectEnvironment />;
export default ProjectEnvironmentConfigPage;

View File

@ -1,11 +0,0 @@
import React from 'react';
import ProjectList from '../../component/project/ProjectList/ProjectList';
import PropTypes from 'prop-types';
const render = ({ history }) => <ProjectList history={history} />;
render.propTypes = {
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -1,14 +0,0 @@
import React from 'react';
import ViewProject from '../../component/project/ProjectView';
import PropTypes from 'prop-types';
const render = ({ match: { params }, history }) => (
<ViewProject projectId={params.id} title="View project" history={history} />
);
render.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -2091,10 +2091,10 @@
resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz" resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz"
integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
"@types/node@14.18.10": "@types/node@14.18.12":
version "14.18.10" version "14.18.12"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.10.tgz#774f43868964f3cfe4ced1f5417fe15818a4eaea" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.12.tgz#0d4557fd3b94497d793efd4e7d92df2f83b4ef24"
integrity sha512-6iihJ/Pp5fsFJ/aEDGyvT4pHGmCpq7ToQ/yf4bl5SbVAvwpspYJ+v3jO7n8UyjhQVHTy+KNszOozDdv+O6sovQ== integrity sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==
"@types/node@^14.14.31": "@types/node@^14.14.31":
version "14.17.19" version "14.17.19"
@ -6426,10 +6426,10 @@ http-proxy-middleware@0.19.1:
lodash "^4.17.11" lodash "^4.17.11"
micromatch "^3.1.10" micromatch "^3.1.10"
http-proxy-middleware@2.0.2: http-proxy-middleware@2.0.3:
version "2.0.2" version "2.0.3"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.2.tgz#94d7593790aad6b3de48164f13792262f656c332" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.3.tgz#5df04f69a89f530c2284cd71eeaa51ba52243289"
integrity sha512-XtmDN5w+vdFTBZaYhdJAbMqn0DP/EhkUaAeo963mojwpKMMbw6nivtFKw07D7DDOH745L5k0VL0P8KRYNEVF/g== integrity sha512-1bloEwnrHMnCoO/Gcwbz7eSVvW50KPES01PecpagI+YLNLci4AcuKJrujW4Mc3sBLpFxMSlsLNHS5Nl/lvrTPA==
dependencies: dependencies:
"@types/http-proxy" "^1.17.8" "@types/http-proxy" "^1.17.8"
http-proxy "^1.18.1" http-proxy "^1.18.1"