mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-14 00:15:52 +01:00
Merge branch 'main' into fix/api-token-copy
This commit is contained in:
commit
def7dbf963
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "unleash-frontend",
|
||||
"description": "unleash your features",
|
||||
"version": "4.8.0-beta.1",
|
||||
"version": "4.8.0-beta.5",
|
||||
"keywords": [
|
||||
"unleash",
|
||||
"feature toggle",
|
||||
@ -46,7 +46,7 @@
|
||||
"@types/debounce": "1.2.1",
|
||||
"@types/deep-diff": "1.0.1",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/node": "14.18.10",
|
||||
"@types/node": "14.18.12",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/react-dom": "17.0.11",
|
||||
"@types/react-outside-click-handler": "1.3.1",
|
||||
@ -63,7 +63,7 @@
|
||||
"debounce": "1.2.1",
|
||||
"deep-diff": "1.0.2",
|
||||
"fast-json-patch": "3.1.0",
|
||||
"http-proxy-middleware": "2.0.2",
|
||||
"http-proxy-middleware": "2.0.3",
|
||||
"@types/lodash.clonedeep": "4.5.6",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"lodash.flow": "3.5.0",
|
||||
|
@ -81,7 +81,7 @@ export const App = () => {
|
||||
<EnvironmentSplash onFinish={refetchSplash} />
|
||||
}
|
||||
elseShow={
|
||||
<LayoutPicker location={location}>
|
||||
<LayoutPicker>
|
||||
<Switch>
|
||||
<ProtectedRoute
|
||||
exact
|
||||
|
@ -5,13 +5,13 @@ import {
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
} 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 {
|
||||
DELETE_ADDON,
|
||||
UPDATE_ADDON,
|
||||
} from '../../../providers/AccessProvider/permissions';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import PageContent from '../../../common/PageContent/PageContent';
|
||||
import useAddons from '../../../../hooks/api/getters/useAddons/useAddons';
|
||||
import useToast from '../../../../hooks/useToast';
|
||||
@ -31,6 +31,7 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
|
||||
const { updateAddon, removeAddon } = useAddonsApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const history = useHistory();
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [deletedAddon, setDeletedAddon] = useState<IAddon>({
|
||||
id: 0,
|
||||
@ -115,10 +116,19 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={addon.enabled}
|
||||
show={<Visibility />}
|
||||
elseShow={<VisibilityOff />}
|
||||
show={<Visibility titleAccess="Disable addon" />}
|
||||
elseShow={<VisibilityOff titleAccess="Enable addon" />}
|
||||
/>
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_ADDON}
|
||||
tooltip={'Edit Addon'}
|
||||
onClick={() => {
|
||||
history.push(`/addons/edit/${addon.id}`);
|
||||
}}
|
||||
>
|
||||
<Edit titleAccess="Edit Addon" />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
permission={DELETE_ADDON}
|
||||
tooltip={'Remove Addon'}
|
||||
@ -127,7 +137,7 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
|
||||
setShowDelete(true);
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
<Delete titleAccess="Remove Addon" />
|
||||
</PermissionIconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
|
@ -1,27 +1,21 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { ApiTokenList } from '../api-token/ApiTokenList/ApiTokenList';
|
||||
import AdminMenu from '../menu/AdminMenu';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import { useContext } from 'react';
|
||||
|
||||
const ApiPage = ({ history }) => {
|
||||
const ApiPage = () => {
|
||||
const { isAdmin } = useContext(AccessContext);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConditionallyRender
|
||||
condition={isAdmin}
|
||||
show={<AdminMenu history={history} />}
|
||||
show={<AdminMenu />}
|
||||
/>
|
||||
<ApiTokenList />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ApiPage.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ApiPage;
|
||||
|
@ -18,7 +18,7 @@ const ProjectRoles = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminMenu history={history} />
|
||||
<AdminMenu />
|
||||
<PageContent
|
||||
bodyClass={styles.rolesListBody}
|
||||
headerContent={
|
||||
|
@ -38,7 +38,6 @@ const EditUser = () => {
|
||||
} = useAddUserForm(
|
||||
user?.name,
|
||||
user?.email,
|
||||
user?.sendEmail,
|
||||
user?.rootRole
|
||||
);
|
||||
|
||||
|
@ -18,7 +18,7 @@ const UsersAdmin = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminMenu history={history} />
|
||||
<AdminMenu />
|
||||
<PageContent
|
||||
bodyClass={styles.userListBody}
|
||||
headerContent={
|
||||
|
@ -114,8 +114,6 @@ const ChangePassword = ({
|
||||
password={data.password}
|
||||
callback={setValidPassword}
|
||||
/>
|
||||
|
||||
<p style={{ color: 'red' }}>{error.general}</p>
|
||||
<TextField
|
||||
label="New password"
|
||||
name="password"
|
||||
|
@ -15,7 +15,6 @@ import {
|
||||
FlagRounded,
|
||||
SvgIconComponent,
|
||||
} from '@material-ui/icons';
|
||||
import { shorten } from '../../common';
|
||||
import {
|
||||
CREATE_FEATURE,
|
||||
CREATE_STRATEGY,
|
||||
@ -87,9 +86,14 @@ export const ApplicationView = () => {
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
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>
|
||||
);
|
||||
|
@ -18,7 +18,6 @@ test('renders correctly if no application', () => {
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
deleteApplication={jest.fn()}
|
||||
history={{}}
|
||||
locationSettings={{ locale: 'en-GB' }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
@ -42,7 +41,6 @@ test('renders correctly without permission', () => {
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
deleteApplication={jest.fn()}
|
||||
history={{}}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
instances: [
|
||||
@ -104,7 +102,6 @@ test('renders correctly with permissions', () => {
|
||||
<ApplicationEdit
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
history={{}}
|
||||
deleteApplication={jest.fn()}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
|
@ -18,7 +18,6 @@ test('renders correctly if no application', () => {
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
deleteApplication={jest.fn()}
|
||||
history={{}}
|
||||
locationSettings={{ locale: 'en-GB' }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
@ -42,7 +41,6 @@ test('renders correctly without permission', () => {
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
deleteApplication={jest.fn()}
|
||||
history={{}}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
instances: [
|
||||
@ -104,7 +102,6 @@ test('renders correctly with permissions', () => {
|
||||
<ApplicationEdit
|
||||
fetchApplication={() => Promise.resolve({})}
|
||||
storeApplicationMetaData={jest.fn()}
|
||||
history={{}}
|
||||
deleteApplication={jest.fn()}
|
||||
application={{
|
||||
appName: 'test-app',
|
||||
|
@ -7,7 +7,7 @@ import useToast from '../../hooks/useToast';
|
||||
import { useFeaturesSort } from '../../hooks/useFeaturesSort';
|
||||
|
||||
export const ArchiveListContainer = () => {
|
||||
const { setToastApiError } = useToast();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { reviveFeature } = useFeatureArchiveApi();
|
||||
const { archivedFeatures, loading, refetchArchived } = useFeaturesArchive();
|
||||
@ -17,6 +17,14 @@ export const ArchiveListContainer = () => {
|
||||
const revive = (feature: string) => {
|
||||
reviveFeature(feature)
|
||||
.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()));
|
||||
};
|
||||
|
||||
|
@ -5,8 +5,16 @@ export const useStyles = makeStyles(theme => ({
|
||||
position: 'absolute',
|
||||
top: '4px',
|
||||
},
|
||||
breadcrumbNavParagraph: { color: 'inherit' },
|
||||
breadcrumbNavParagraph: {
|
||||
color: 'inherit',
|
||||
'& > *': {
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
},
|
||||
breadcrumbLink: {
|
||||
textDecoration: 'none',
|
||||
'& > *': {
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
@ -4,6 +4,7 @@ import ConditionallyRender from '../ConditionallyRender';
|
||||
import { useStyles } from './BreadcrumbNav.styles';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import { useContext } from 'react';
|
||||
import StringTruncator from '../StringTruncator/StringTruncator';
|
||||
|
||||
const BreadcrumbNav = () => {
|
||||
const { isAdmin } = useContext(AccessContext);
|
||||
@ -51,7 +52,7 @@ const BreadcrumbNav = () => {
|
||||
styles.breadcrumbNavParagraph
|
||||
}
|
||||
>
|
||||
{path.substring(0, 30)}
|
||||
<StringTruncator text={path} maxWidth="200" />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@ -72,7 +73,7 @@ const BreadcrumbNav = () => {
|
||||
className={styles.breadcrumbLink}
|
||||
to={link}
|
||||
>
|
||||
{path.substring(0, 30)}
|
||||
<StringTruncator text={path} maxWidth="200" />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
@ -9,7 +9,7 @@ interface IPermissionIconButtonProps
|
||||
> {
|
||||
permission: string;
|
||||
Icon?: React.ElementType;
|
||||
tooltip: string;
|
||||
tooltip?: string;
|
||||
onClick?: (e: any) => void;
|
||||
projectId?: string;
|
||||
environmentId?: string;
|
||||
|
@ -72,8 +72,6 @@ const ProjectSelect = ({ currentProjectId, updateCurrentProject, ...rest }) => {
|
||||
};
|
||||
|
||||
ProjectSelect.propTypes = {
|
||||
projects: PropTypes.array.isRequired,
|
||||
fetchProjects: PropTypes.func.isRequired,
|
||||
currentProjectId: PropTypes.string.isRequired,
|
||||
updateCurrentProject: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -38,6 +38,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
|
||||
permission={permission}
|
||||
projectId={projectId}
|
||||
environmentId={environmentId}
|
||||
tooltip={tooltip}
|
||||
data-loading
|
||||
{...rest}
|
||||
>
|
||||
@ -53,6 +54,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
|
||||
variant="contained"
|
||||
disabled={disabled}
|
||||
environmentId={environmentId}
|
||||
tooltip={tooltip}
|
||||
data-loading
|
||||
{...rest}
|
||||
>
|
||||
|
@ -19,8 +19,6 @@ import ConditionallyRender from './ConditionallyRender/ConditionallyRender';
|
||||
|
||||
export { styles };
|
||||
|
||||
export const shorten = (str, len = 50) =>
|
||||
str && str.length > len ? `${str.substring(0, len)}...` : str;
|
||||
export const AppsLinkList = ({ apps }) => (
|
||||
<List>
|
||||
<ConditionallyRender
|
||||
|
@ -112,11 +112,11 @@ const ContextForm: React.FC<IContextForm> = ({
|
||||
autoFocus
|
||||
/>
|
||||
<p className={styles.inputDescription}>
|
||||
What is this context for?
|
||||
What is this context for?
|
||||
</p>
|
||||
<TextField
|
||||
className={styles.input}
|
||||
label="Context description"
|
||||
label="Context description (optional)"
|
||||
variant="outlined"
|
||||
multiline
|
||||
maxRows={4}
|
||||
@ -139,7 +139,7 @@ const ContextForm: React.FC<IContextForm> = ({
|
||||
})}
|
||||
<div className={styles.tagContainer}>
|
||||
<TextField
|
||||
label="Value"
|
||||
label="Value (optional)"
|
||||
name="value"
|
||||
className={styles.tagInput}
|
||||
value={value}
|
||||
|
@ -159,7 +159,6 @@ const EnvironmentListItem = ({
|
||||
<Tooltip title={`${tooltipText} environment`}>
|
||||
<IconButton
|
||||
aria-label="disable"
|
||||
disabled={env.protected}
|
||||
onClick={() => {
|
||||
setSelectedEnv(env);
|
||||
setToggleDialog(prev => !prev);
|
||||
|
@ -55,7 +55,6 @@ const EditFeature = () => {
|
||||
history.push(`/projects/${project}/features/${name}`);
|
||||
setToastData({
|
||||
title: 'Toggle updated successfully',
|
||||
text: 'Now you can start using your toggle.',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e: any) {
|
||||
|
@ -5,6 +5,9 @@ import { DialogContentText } from '@material-ui/core';
|
||||
import ConditionallyRender from '../../../../common/ConditionallyRender/ConditionallyRender';
|
||||
import Dialogue from '../../../../common/Dialogue';
|
||||
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 {
|
||||
open: boolean;
|
||||
@ -13,6 +16,7 @@ interface IStaleDialogProps {
|
||||
}
|
||||
|
||||
const StaleDialog = ({ open, setOpen, stale }: IStaleDialogProps) => {
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||
const { patchFeatureToggle } = useFeatureApi();
|
||||
const { refetch } = useFeature(projectId, featureId);
|
||||
@ -30,12 +34,31 @@ const StaleDialog = ({ open, setOpen, stale }: IStaleDialogProps) => {
|
||||
|
||||
const toggleActionText = stale ? 'active' : 'stale';
|
||||
|
||||
const onSubmit = async e => {
|
||||
e.stopPropagation();
|
||||
const patch = [{ op: 'replace', path: '/stale', value: !stale }];
|
||||
await patchFeatureToggle(projectId, featureId, patch);
|
||||
refetch();
|
||||
setOpen(false);
|
||||
const onSubmit = async (event: React.SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
try {
|
||||
const patch = [{ op: 'replace', path: '/stale', value: !stale }];
|
||||
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 = () => {
|
||||
|
@ -44,4 +44,7 @@ export const useStyles = makeStyles(theme => ({
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
featureId: {
|
||||
wordBreak: 'break-all'
|
||||
}
|
||||
}));
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Tab, Tabs, useMediaQuery } from '@material-ui/core';
|
||||
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 useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
|
||||
@ -115,7 +115,8 @@ const FeatureView = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
The feature <strong>{featureId.substring(0, 30)}</strong>{' '}
|
||||
The feature{' '}
|
||||
<strong className={styles.featureId}>{featureId}</strong>{' '}
|
||||
does not exist. Do you want to
|
||||
<Link
|
||||
to={getCreateTogglePath(projectId, uiConfig.flags.E, {
|
||||
|
@ -29,9 +29,10 @@ const useFeatureForm = (
|
||||
}, [initialType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!toggleQueryName) setName(initialName);
|
||||
else setName(toggleQueryName);
|
||||
}, [initialName, toggleQueryName]);
|
||||
if (!name) {
|
||||
setName(toggleQueryName || initialName);
|
||||
}
|
||||
}, [name, initialName, toggleQueryName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId) setProject(initialProject);
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { useContext } from 'react';
|
||||
import { ADMIN } from '../../component/providers/AccessProvider/permissions';
|
||||
import ConditionallyRender from '../../component/common/ConditionallyRender';
|
||||
import { EventHistory } from '../../component/history/EventHistory/EventHistory';
|
||||
import AccessContext from '../../contexts/AccessContext';
|
||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import { EventHistory } from '../EventHistory/EventHistory';
|
||||
|
||||
const HistoryPage = () => {
|
||||
export const EventHistoryPage = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
|
||||
return (
|
||||
@ -20,5 +20,3 @@ const HistoryPage = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryPage;
|
@ -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} />;
|
||||
};
|
@ -1,8 +1,10 @@
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import { matchPath } from 'react-router';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { MainLayout } from '../MainLayout/MainLayout';
|
||||
|
||||
const LayoutPicker = ({ children, location }) => {
|
||||
const LayoutPicker = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const standalonePages = () => {
|
||||
const { pathname } = location;
|
||||
const isLoginPage = matchPath(pathname, { path: '/login' });
|
||||
|
@ -27,7 +27,7 @@ const Header = () => {
|
||||
const [anchorEl, setAnchorEl] = useState();
|
||||
const [anchorElAdvanced, setAnchorElAdvanced] = useState();
|
||||
const [admin, setAdmin] = useState(false);
|
||||
const { permissions } = useAuthPermissions()
|
||||
const { permissions } = useAuthPermissions();
|
||||
const commonStyles = useCommonStyles();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
@ -68,12 +68,19 @@ const Header = () => {
|
||||
className={styles.drawerButton}
|
||||
onClick={toggleDrawer}
|
||||
>
|
||||
<MenuIcon />
|
||||
<MenuIcon titleAccess="Menu" />
|
||||
</IconButton>
|
||||
}
|
||||
elseShow={
|
||||
<Link to="/" className={commonStyles.flexRow}>
|
||||
<UnleashLogo className={styles.logo} />{' '}
|
||||
<Link
|
||||
to="/"
|
||||
className={commonStyles.flexRow}
|
||||
aria-label="Home"
|
||||
>
|
||||
<UnleashLogo
|
||||
className={styles.logo}
|
||||
aria-label="Unleash logo"
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
@ -127,6 +134,7 @@ const Header = () => {
|
||||
>
|
||||
<MenuBookIcon
|
||||
className={styles.docsIcon}
|
||||
titleAccess="Documentation"
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
@ -140,6 +148,7 @@ const Header = () => {
|
||||
>
|
||||
<SettingsIcon
|
||||
className={styles.docsIcon}
|
||||
titleAccess="Settings"
|
||||
/>
|
||||
</IconButton>
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { FeatureToggleListContainer } from '../feature/FeatureToggleList/FeatureToggleListContainer';
|
||||
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 HistoryPage from '../../page/history';
|
||||
import HistoryTogglePage from '../../page/history/toggle';
|
||||
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
|
||||
import { TagTypeList } from '../tags/TagTypeList/TagTypeList';
|
||||
import { AddonList } from '../addons/AddonList/AddonList';
|
||||
@ -45,6 +43,8 @@ import RedirectFeatureView from '../feature/RedirectFeatureView/RedirectFeatureV
|
||||
import { CreateAddon } from '../addons/CreateAddon/CreateAddon';
|
||||
import { EditAddon } from '../addons/EditAddon/EditAddon';
|
||||
import { CopyFeatureToggle } from '../feature/CopyFeature/CopyFeature';
|
||||
import { EventHistoryPage } from '../history/EventHistoryPage/EventHistoryPage';
|
||||
import { FeatureEventHistoryPage } from '../history/FeatureEventHistoryPage/FeatureEventHistoryPage';
|
||||
|
||||
export const routes = [
|
||||
// Project
|
||||
@ -354,7 +354,7 @@ export const routes = [
|
||||
path: '/history/:toggleName',
|
||||
title: ':toggleName',
|
||||
parent: '/history',
|
||||
component: HistoryTogglePage,
|
||||
component: FeatureEventHistoryPage,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
@ -362,7 +362,7 @@ export const routes = [
|
||||
{
|
||||
path: '/history',
|
||||
title: 'Event History',
|
||||
component: HistoryPage,
|
||||
component: EventHistoryPage,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: { adminSettings: true },
|
||||
|
@ -32,4 +32,18 @@ export const useStyles = makeStyles(theme => ({
|
||||
width: 'auto',
|
||||
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',
|
||||
},
|
||||
}));
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useHistory, useParams } from 'react-router';
|
||||
import { useCommonStyles } from '../../../common.styles';
|
||||
import useProject from '../../../hooks/api/getters/useProject/useProject';
|
||||
import useLoading from '../../../hooks/useLoading';
|
||||
import ApiError from '../../common/ApiError/ApiError';
|
||||
@ -25,7 +24,6 @@ const Project = () => {
|
||||
const { project, error, loading, refetch } = useProject(id);
|
||||
const ref = useLoading(loading);
|
||||
const { setToastData } = useToast();
|
||||
const commonStyles = useCommonStyles();
|
||||
const styles = useStyles();
|
||||
const history = useHistory();
|
||||
|
||||
@ -121,10 +119,10 @@ const Project = () => {
|
||||
<div className={styles.innerContainer}>
|
||||
<h2
|
||||
data-loading
|
||||
className={commonStyles.title}
|
||||
className={styles.title}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
Project: {project?.name}{' '}
|
||||
<div className={styles.titleText}>{project?.name}</div>
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_PROJECT}
|
||||
tooltip="Edit"
|
||||
|
@ -23,6 +23,11 @@ export const useStyles = makeStyles(theme => ({
|
||||
title: {
|
||||
fontWeight: 'normal',
|
||||
fontSize: '1rem',
|
||||
lineClamp: 2,
|
||||
display: '-webkit-box',
|
||||
boxOrient: 'vertical',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
projectIcon: {
|
||||
|
@ -46,7 +46,7 @@ const ProjectCard = ({
|
||||
return (
|
||||
<Card className={styles.projectCard} onMouseEnter={onHover}>
|
||||
<div className={styles.header} data-loading>
|
||||
<h2 className={styles.title}>{name}</h2>
|
||||
<div className={styles.title}>{name}</div>
|
||||
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_PROJECT}
|
||||
|
@ -11,6 +11,7 @@ import useStrategiesApi from '../../../hooks/api/actions/useStrategiesApi/useStr
|
||||
import { IStrategy } from '../../../interfaces/strategy';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies';
|
||||
import { formatUnknownError } from '../../../utils/format-unknown-error';
|
||||
|
||||
interface ICustomStrategyParams {
|
||||
name?: string;
|
||||
@ -78,7 +79,7 @@ export const StrategyForm = ({ editMode, strategy }: IStrategyFormProps) => {
|
||||
setParams(prev => [...parameters]);
|
||||
if (editMode) {
|
||||
try {
|
||||
await updateStrategy({ name, description, parameters: params });
|
||||
await updateStrategy({ name, description, parameters });
|
||||
history.push(`/strategies/view/${name}`);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
@ -86,12 +87,12 @@ export const StrategyForm = ({ editMode, strategy }: IStrategyFormProps) => {
|
||||
text: 'Successfully updated strategy',
|
||||
});
|
||||
refetchStrategies();
|
||||
} catch (e: any) {
|
||||
setToastApiError(e.toString());
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await createStrategy({ name, description, parameters: params });
|
||||
await createStrategy({ name, description, parameters });
|
||||
history.push(`/strategies`);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
@ -99,13 +100,8 @@ export const StrategyForm = ({ editMode, strategy }: IStrategyFormProps) => {
|
||||
text: 'Successfully created new strategy',
|
||||
});
|
||||
refetchStrategies();
|
||||
} catch (e: any) {
|
||||
const STRATEGY_EXIST_ERROR = 'Error: Strategy with name';
|
||||
if (e.toString().includes(STRATEGY_EXIST_ERROR)) {
|
||||
setErrors({
|
||||
name: 'A strategy with this name already exists',
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -23,7 +23,6 @@ test('renders correctly with one strategy', () => {
|
||||
removeStrategy={jest.fn()}
|
||||
deprecateStrategy={jest.fn()}
|
||||
reactivateStrategy={jest.fn()}
|
||||
history={{}}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</UIProvider>
|
||||
@ -50,7 +49,6 @@ test('renders correctly with one strategy without permissions', () => {
|
||||
removeStrategy={jest.fn()}
|
||||
deprecateStrategy={jest.fn()}
|
||||
reactivateStrategy={jest.fn()}
|
||||
history={{}}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</UIProvider>
|
||||
|
@ -45,7 +45,6 @@ test('renders correctly with one strategy', () => {
|
||||
fetchStrategies={jest.fn()}
|
||||
fetchApplications={jest.fn()}
|
||||
fetchFeatureToggles={jest.fn()}
|
||||
history={{}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</AccessProvider>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
|
||||
import { formatUnknownError } from '../../../utils/format-unknown-error';
|
||||
|
||||
const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => {
|
||||
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 () => {
|
||||
if (tagName.length === 0) {
|
||||
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
|
||||
@ -39,14 +38,12 @@ const useTagTypeForm = (initialTagName = '', initialTagDesc = '') => {
|
||||
try {
|
||||
await validateTagName(tagName);
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
if (e.toString().includes(NAME_EXISTS_ERROR)) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
name: NAME_EXISTS_ERROR,
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
name: formatUnknownError(err)
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -22,7 +22,6 @@ test('renders an empty list correctly', () => {
|
||||
tagTypes={[]}
|
||||
fetchTagTypes={jest.fn()}
|
||||
removeTagType={jest.fn()}
|
||||
history={{}}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</UIProvider>
|
||||
@ -53,7 +52,6 @@ test('renders a list with elements correctly', () => {
|
||||
]}
|
||||
fetchTagTypes={jest.fn()}
|
||||
removeTagType={jest.fn()}
|
||||
history={{}}
|
||||
/>
|
||||
</AccessProvider>
|
||||
</UIProvider>
|
||||
|
@ -10,8 +10,13 @@ import AuthOptions from '../common/AuthOptions/AuthOptions';
|
||||
import DividerText from '../../common/DividerText/DividerText';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
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 {
|
||||
LOGIN_BUTTON,
|
||||
LOGIN_EMAIL_ID,
|
||||
LOGIN_PASSWORD_ID,
|
||||
} from '../../../testIds';
|
||||
|
||||
const HostedAuth = ({ authDetails }) => {
|
||||
const commonStyles = useCommonStyles();
|
||||
@ -19,7 +24,7 @@ const HostedAuth = ({ authDetails }) => {
|
||||
const { refetchUser } = useAuthUser();
|
||||
const history = useHistory();
|
||||
const params = useQueryParams();
|
||||
const { passwordAuth } = useAuthApi()
|
||||
const { passwordAuth } = useAuthApi();
|
||||
const [username, setUsername] = useState(params.get('email') || '');
|
||||
const [password, setPassword] = useState('');
|
||||
const [errors, setErrors] = useState({
|
||||
@ -108,6 +113,7 @@ const HostedAuth = ({ authDetails }) => {
|
||||
helperText={usernameError}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
data-test={LOGIN_EMAIL_ID}
|
||||
/>
|
||||
<PasswordField
|
||||
label="Password"
|
||||
@ -116,6 +122,7 @@ const HostedAuth = ({ authDetails }) => {
|
||||
value={password}
|
||||
error={!!passwordError}
|
||||
helperText={passwordError}
|
||||
data-test={LOGIN_PASSWORD_ID}
|
||||
/>
|
||||
<Grid container>
|
||||
<Button
|
||||
@ -123,6 +130,7 @@ const HostedAuth = ({ authDetails }) => {
|
||||
color="primary"
|
||||
type="submit"
|
||||
className={styles.button}
|
||||
data-test={LOGIN_BUTTON}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
|
@ -14,10 +14,11 @@ export const handleBadRequest = async (
|
||||
if (!setErrors || !requestId) return;
|
||||
if (res) {
|
||||
const data = await res.json();
|
||||
const message = data.isJoi ? data.details[0].message : data[0].msg;
|
||||
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[requestId]: data[0].msg,
|
||||
[requestId]: message,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -47,10 +48,11 @@ export const handleUnauthorized = async (
|
||||
if (!setErrors || !requestId) return;
|
||||
if (res) {
|
||||
const data = await res.json();
|
||||
const message = data.isJoi ? data.details[0].message : data[0].msg;
|
||||
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[requestId]: data[0].msg,
|
||||
[requestId]: message,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -65,7 +67,6 @@ export const handleForbidden = async (
|
||||
if (!setErrors || !requestId) return;
|
||||
if (res) {
|
||||
const data = await res.json();
|
||||
|
||||
const message = data.isJoi ? data.details[0].message : data[0].msg;
|
||||
|
||||
setErrors(prev => ({
|
||||
|
@ -58,7 +58,3 @@ export const useAuthApi = (): IUseAuthApiOutput => {
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
const ensureRelativePath = (path: string): string => {
|
||||
return path.replace(/^\//, '');
|
||||
};
|
||||
|
@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import ClientInstance from '../../component/client-instance/client-instance-container';
|
||||
|
||||
const render = () => <ClientInstance />;
|
||||
|
||||
export default render;
|
@ -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;
|
@ -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;
|
@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import Metrics from '../../component/metrics/metrics-container';
|
||||
|
||||
const render = () => <Metrics />;
|
||||
|
||||
export default render;
|
@ -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;
|
@ -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;
|
@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import ProjectEnvironment from '../../component/project/ProjectEnvironment/ProjectEnvironment';
|
||||
|
||||
const ProjectEnvironmentConfigPage = () => <ProjectEnvironment />;
|
||||
|
||||
export default ProjectEnvironmentConfigPage;
|
@ -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;
|
@ -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;
|
@ -2091,10 +2091,10 @@
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz"
|
||||
integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
|
||||
|
||||
"@types/node@14.18.10":
|
||||
version "14.18.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.10.tgz#774f43868964f3cfe4ced1f5417fe15818a4eaea"
|
||||
integrity sha512-6iihJ/Pp5fsFJ/aEDGyvT4pHGmCpq7ToQ/yf4bl5SbVAvwpspYJ+v3jO7n8UyjhQVHTy+KNszOozDdv+O6sovQ==
|
||||
"@types/node@14.18.12":
|
||||
version "14.18.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.12.tgz#0d4557fd3b94497d793efd4e7d92df2f83b4ef24"
|
||||
integrity sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==
|
||||
|
||||
"@types/node@^14.14.31":
|
||||
version "14.17.19"
|
||||
@ -6426,10 +6426,10 @@ http-proxy-middleware@0.19.1:
|
||||
lodash "^4.17.11"
|
||||
micromatch "^3.1.10"
|
||||
|
||||
http-proxy-middleware@2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.2.tgz#94d7593790aad6b3de48164f13792262f656c332"
|
||||
integrity sha512-XtmDN5w+vdFTBZaYhdJAbMqn0DP/EhkUaAeo963mojwpKMMbw6nivtFKw07D7DDOH745L5k0VL0P8KRYNEVF/g==
|
||||
http-proxy-middleware@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.3.tgz#5df04f69a89f530c2284cd71eeaa51ba52243289"
|
||||
integrity sha512-1bloEwnrHMnCoO/Gcwbz7eSVvW50KPES01PecpagI+YLNLci4AcuKJrujW4Mc3sBLpFxMSlsLNHS5Nl/lvrTPA==
|
||||
dependencies:
|
||||
"@types/http-proxy" "^1.17.8"
|
||||
http-proxy "^1.18.1"
|
||||
|
Loading…
Reference in New Issue
Block a user