diff --git a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx index 233d57354b..78ef5188cc 100644 --- a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx +++ b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx @@ -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({ id: 0, @@ -115,10 +116,19 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => { > } - elseShow={} + show={} + elseShow={} /> + { + history.push(`/addons/edit/${addon.id}`); + }} + > + + { setShowDelete(true); }} > - + diff --git a/frontend/src/component/admin/users/EditUser/EditUser.tsx b/frontend/src/component/admin/users/EditUser/EditUser.tsx index a42e4e6de0..e5db03f2b1 100644 --- a/frontend/src/component/admin/users/EditUser/EditUser.tsx +++ b/frontend/src/component/admin/users/EditUser/EditUser.tsx @@ -38,7 +38,6 @@ const EditUser = () => { } = useAddUserForm( user?.name, user?.email, - user?.sendEmail, user?.rootRole ); diff --git a/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx b/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx index 976a6ec8d2..bd0cb82fc5 100644 --- a/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx +++ b/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx @@ -114,8 +114,6 @@ const ChangePassword = ({ password={data.password} callback={setValidPassword} /> - -

{error.general}

{ {shorten(name, 50)} + + {name} + } - secondary={shorten(description, 60)} + secondary={description} /> ); diff --git a/frontend/src/component/archive/ArchiveListContainer.tsx b/frontend/src/component/archive/ArchiveListContainer.tsx index 3ce133bc57..698259fd57 100644 --- a/frontend/src/component/archive/ArchiveListContainer.tsx +++ b/frontend/src/component/archive/ArchiveListContainer.tsx @@ -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())); }; diff --git a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.styles.ts b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.styles.ts index a551eba549..d1dfa48d38 100644 --- a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.styles.ts +++ b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.styles.ts @@ -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', + }, }, })); diff --git a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx index b08c182b97..d299010ca6 100644 --- a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx +++ b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx @@ -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)} +

); } @@ -72,7 +73,7 @@ const BreadcrumbNav = () => { className={styles.breadcrumbLink} to={link} > - {path.substring(0, 30)} + ); })} diff --git a/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx b/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx index ff6103efd2..d987e05a94 100644 --- a/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx +++ b/frontend/src/component/common/PermissionIconButton/PermissionIconButton.tsx @@ -9,7 +9,7 @@ interface IPermissionIconButtonProps > { permission: string; Icon?: React.ElementType; - tooltip: string; + tooltip?: string; onClick?: (e: any) => void; projectId?: string; environmentId?: string; diff --git a/frontend/src/component/common/ResponsiveButton/ResponsiveButton.tsx b/frontend/src/component/common/ResponsiveButton/ResponsiveButton.tsx index aa18fd1ff0..0edb2cb94f 100644 --- a/frontend/src/component/common/ResponsiveButton/ResponsiveButton.tsx +++ b/frontend/src/component/common/ResponsiveButton/ResponsiveButton.tsx @@ -38,6 +38,7 @@ const ResponsiveButton: React.FC = ({ permission={permission} projectId={projectId} environmentId={environmentId} + tooltip={tooltip} data-loading {...rest} > @@ -53,6 +54,7 @@ const ResponsiveButton: React.FC = ({ variant="contained" disabled={disabled} environmentId={environmentId} + tooltip={tooltip} data-loading {...rest} > diff --git a/frontend/src/component/common/index.js b/frontend/src/component/common/index.js index 8d437a6e0a..ffc2fe9395 100644 --- a/frontend/src/component/common/index.js +++ b/frontend/src/component/common/index.js @@ -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 }) => ( = ({ autoFocus />

- What is this context for? + What is this context for?

= ({ })}
{ history.push(`/projects/${project}/features/${name}`); setToastData({ title: 'Toggle updated successfully', - text: 'Now you can start using your toggle.', type: 'success', }); } catch (e: any) { diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/StaleDialog/StaleDialog.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/StaleDialog/StaleDialog.tsx index ab7a3473e0..51464afe25 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/StaleDialog/StaleDialog.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/StaleDialog/StaleDialog.tsx @@ -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(); 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 = () => { diff --git a/frontend/src/component/feature/FeatureView/FeatureView.styles.ts b/frontend/src/component/feature/FeatureView/FeatureView.styles.ts index 3b59a9fd91..1677064e08 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.styles.ts +++ b/frontend/src/component/feature/FeatureView/FeatureView.styles.ts @@ -44,4 +44,7 @@ export const useStyles = makeStyles(theme => ({ flexDirection: 'column', }, }, + featureId: { + wordBreak: 'break-all' + } })); diff --git a/frontend/src/component/feature/FeatureView/FeatureView.tsx b/frontend/src/component/feature/FeatureView/FeatureView.tsx index a612c964eb..031bee5280 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.tsx @@ -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 (

- The feature {featureId.substring(0, 30)}{' '} + The feature{' '} + {featureId}{' '} does not exist. Do you want to   { - if (!toggleQueryName) setName(initialName); - else setName(toggleQueryName); - }, [initialName, toggleQueryName]); + if (!name) { + setName(toggleQueryName || initialName); + } + }, [name, initialName, toggleQueryName]); useEffect(() => { if (!projectId) setProject(initialProject); diff --git a/frontend/src/component/menu/Header/Header.tsx b/frontend/src/component/menu/Header/Header.tsx index 516233ddca..f83ac83a07 100644 --- a/frontend/src/component/menu/Header/Header.tsx +++ b/frontend/src/component/menu/Header/Header.tsx @@ -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} > - + } elseShow={ - - {' '} + + } /> @@ -127,6 +134,7 @@ const Header = () => { > @@ -140,6 +148,7 @@ const Header = () => { > } diff --git a/frontend/src/component/project/Project/Project.styles.ts b/frontend/src/component/project/Project/Project.styles.ts index 7658ac653f..70b3461a19 100644 --- a/frontend/src/component/project/Project/Project.styles.ts +++ b/frontend/src/component/project/Project/Project.styles.ts @@ -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', + }, })); diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx index 29f7468de5..c61f71dbe8 100644 --- a/frontend/src/component/project/Project/Project.tsx +++ b/frontend/src/component/project/Project/Project.tsx @@ -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 = () => {

- Project: {project?.name}{' '} +
{project?.name}
({ title: { fontWeight: 'normal', fontSize: '1rem', + lineClamp: 2, + display: '-webkit-box', + boxOrient: 'vertical', + textOverflow: 'ellipsis', + overflow: 'hidden' }, projectIcon: { diff --git a/frontend/src/component/project/ProjectCard/ProjectCard.tsx b/frontend/src/component/project/ProjectCard/ProjectCard.tsx index 3fc61331f5..6b4c71f7c2 100644 --- a/frontend/src/component/project/ProjectCard/ProjectCard.tsx +++ b/frontend/src/component/project/ProjectCard/ProjectCard.tsx @@ -46,7 +46,7 @@ const ProjectCard = ({ return (
-

{name}

+
{name}
{ 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)); } } }; diff --git a/frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts b/frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts index fe7452914e..976e3fc539 100644 --- a/frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts +++ b/frontend/src/component/tags/TagTypeForm/useTagTypeForm.ts @@ -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; } }; diff --git a/frontend/src/hooks/api/actions/useAdminUsersApi/errorHandlers.ts b/frontend/src/hooks/api/actions/useAdminUsersApi/errorHandlers.ts index 522bab536e..0761089d09 100644 --- a/frontend/src/hooks/api/actions/useAdminUsersApi/errorHandlers.ts +++ b/frontend/src/hooks/api/actions/useAdminUsersApi/errorHandlers.ts @@ -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 => ({