1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

refactor: improve icon labels and tooltips (#884)

This commit is contained in:
olav 2022-04-21 08:26:49 +02:00 committed by GitHub
parent 8a3db090d5
commit 2e5e25bfe5
63 changed files with 479 additions and 311 deletions

View File

@ -113,29 +113,30 @@ export const ConfiguredAddons = ({ getAddonIcon }: IConfigureAddonsProps) => {
<PermissionIconButton
permission={UPDATE_ADDON}
onClick={() => toggleAddon(addon)}
tooltip="Toggle addon"
>
<ConditionallyRender
condition={addon.enabled}
show={<Visibility titleAccess="Disable addon" />}
elseShow={<VisibilityOff titleAccess="Enable addon" />}
show={<Visibility />}
elseShow={<VisibilityOff />}
/>
</PermissionIconButton>
<PermissionIconButton
permission={UPDATE_ADDON}
onClick={() => {
history.push(`/addons/edit/${addon.id}`);
}}
tooltip="Edit Addon"
onClick={() => history.push(`/addons/edit/${addon.id}`)}
>
<Edit titleAccess="Edit Addon" />
<Edit />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_ADDON}
tooltip="Remove Addon"
onClick={() => {
setDeletedAddon(addon);
setShowDelete(true);
}}
>
<Delete titleAccess="Remove Addon" />
<Delete />
</PermissionIconButton>
</ListItemSecondaryAction>
</ListItem>

View File

@ -8,6 +8,7 @@ import {
TableCell,
TableHead,
TableRow,
Tooltip,
} from '@material-ui/core';
import AccessContext from 'contexts/AccessContext';
import useToast from 'hooks/useToast';
@ -200,24 +201,28 @@ export const ApiTokenList = () => {
<Secret value={item.secret} />
</TableCell>
<TableCell className={styles.actionsContainer}>
<IconButton
onClick={() => {
copyToken(item.secret);
}}
>
<FileCopy />
</IconButton>
<Tooltip title="Copy token">
<IconButton
onClick={() => {
copyToken(item.secret);
}}
>
<FileCopy />
</IconButton>
</Tooltip>
<ConditionallyRender
condition={hasAccess(DELETE_API_TOKEN)}
show={
<IconButton
onClick={() => {
setDeleteToken(item);
setShowDelete(true);
}}
>
<Delete />
</IconButton>
<Tooltip title="Delete token">
<IconButton
onClick={() => {
setDeleteToken(item);
setShowDelete(true);
}}
>
<Delete />
</IconButton>
</Tooltip>
}
/>
</TableCell>

View File

@ -1,4 +1,4 @@
import { IconButton } from '@material-ui/core';
import { IconButton, Tooltip } from '@material-ui/core';
import CopyIcon from '@material-ui/icons/FileCopy';
import copy from 'copy-to-clipboard';
import useToast from 'hooks/useToast';
@ -14,8 +14,7 @@ export const UserToken = ({ token }: IUserTokenProps) => {
if (copy(token)) {
setToastData({
type: 'success',
title: 'Token copied',
text: `Token is copied to clipboard`,
title: 'Token copied to clipboard',
});
} else
setToastData({
@ -38,9 +37,11 @@ export const UserToken = ({ token }: IUserTokenProps) => {
}}
>
{token}
<IconButton onClick={copyToken}>
<CopyIcon />
</IconButton>
<Tooltip title="Copy token">
<IconButton onClick={copyToken}>
<CopyIcon />
</IconButton>
</Tooltip>
</div>
);
};

View File

@ -121,7 +121,12 @@ const EnvironmentPermissionAccordion = ({
<Accordion style={{ boxShadow: 'none' }}>
<AccordionSummary
className={styles.accordionSummary}
expandIcon={<ExpandMore className={styles.icon} />}
expandIcon={
<ExpandMore
className={styles.icon}
titleAccess="Toggle"
/>
}
>
<div className={styles.accordionHeader}>
<StringTruncator

View File

@ -50,18 +50,17 @@ const RoleListItem = ({
<TableCell align="right">
<PermissionIconButton
data-loading
aria-label="Edit"
disabled={type === BUILTIN_ROLE_TYPE}
onClick={() => {
history.push(`/admin/roles/${id}/edit`);
}}
permission={ADMIN}
tooltip="Edit role"
>
<Edit />
</PermissionIconButton>
<PermissionIconButton
data-loading
aria-label="Remove role"
disabled={type === BUILTIN_ROLE_TYPE}
onClick={() => {
// @ts-expect-error
@ -69,6 +68,7 @@ const RoleListItem = ({
setDelDialog(true);
}}
permission={ADMIN}
tooltip="Remove role"
>
<Delete />
</PermissionIconButton>

View File

@ -1,4 +1,4 @@
import { IconButton } from '@material-ui/core';
import { IconButton, Tooltip } from '@material-ui/core';
import CopyIcon from '@material-ui/icons/FileCopy';
import useToast from 'hooks/useToast';
@ -47,9 +47,11 @@ const UserInviteLink = ({ inviteLink }: IInviteLinkProps) => {
}}
>
{inviteLink}
<IconButton onClick={handleCopy}>
<CopyIcon />
</IconButton>
<Tooltip title="Copy link">
<IconButton onClick={handleCopy}>
<CopyIcon />
</IconButton>
</Tooltip>
</div>
);
};

View File

@ -87,7 +87,6 @@ const UserListItem = ({
<Tooltip title="Edit user" arrow>
<IconButton
data-loading
aria-label="Edit user"
onClick={() =>
history.push(`/admin/users/${user.id}/edit`)
}
@ -98,7 +97,6 @@ const UserListItem = ({
<Tooltip title="Change password" arrow>
<IconButton
data-loading
aria-label="Change password"
onClick={openPwDialog(user)}
>
<Lock />
@ -107,7 +105,6 @@ const UserListItem = ({
<Tooltip title="Remove user" arrow>
<IconButton
data-loading
aria-label="Remove user"
onClick={openDelDialog(user)}
>
<Delete />

View File

@ -112,13 +112,13 @@ export const ApplicationEdit = () => {
condition={Boolean(url)}
show={
<IconButton component={Link} href={url}>
<LinkIcon />
<LinkIcon titleAccess={url} />
</IconButton>
}
/>
<PermissionButton
title="Delete application"
tooltip="Delete application"
onClick={toggleModal}
permission={UPDATE_APPLICATION}
>

View File

@ -22,7 +22,7 @@ export const ApplicationList = () => {
const renderNoApplications = () => (
<>
<section style={{ textAlign: 'center' }}>
<Warning /> <br />
<Warning titleAccess="Warning" /> <br />
<br />
Oh snap, it does not seem like you have connected any
applications. To connect your application to Unleash you will

View File

@ -46,7 +46,7 @@ export const AutocompleteBox = ({
classes={{ inputRoot: styles.inputRoot }}
options={options}
value={value}
popupIcon={<ArrowDropDown />}
popupIcon={<ArrowDropDown titleAccess="Toggle" />}
onChange={(event, value) => onChange(value || [])}
renderInput={renderInput}
getOptionLabel={value => value.label}

View File

@ -12,7 +12,7 @@ const CheckMarkBadge = ({ type, className }: ICheckMarkBadgeProps) => {
return (
<div className={classnames(styles.badge, className)}>
{type === 'error' ? (
<Close className={styles.check} />
<Close className={styles.check} titleAccess="Error" />
) : (
<Check className={styles.check} />
)}

View File

@ -17,6 +17,7 @@ import {
operatorsForContext,
CURRENT_TIME_CONTEXT_FIELD,
} from 'utils/operatorsForContext';
import { Tooltip } from '@material-ui/core';
interface IConstraintAccordionViewHeader {
localConstraint: IConstraint;
@ -121,14 +122,16 @@ export const ConstraintAccordionEditHeader = ({
show={<p className={styles.editingBadge}>Updating...</p>}
elseShow={<p className={styles.editingBadge}>Editing</p>}
/>
<a
href="https://docs.getunleash.io/advanced/strategy_constraints"
style={{ marginLeft: 'auto' }}
target="_blank"
rel="noopener noreferrer"
>
<Help className={styles.help} />
</a>
<Tooltip title="Help">
<a
href="https://docs.getunleash.io/advanced/strategy_constraints"
style={{ marginLeft: 'auto' }}
target="_blank"
rel="noopener noreferrer"
>
<Help className={styles.help} />
</a>
</Tooltip>
</div>
);
};

View File

@ -45,7 +45,7 @@ export const ConstraintAccordionView = ({
>
<AccordionSummary
className={styles.summary}
expandIcon={<ExpandMore />}
expandIcon={<ExpandMore titleAccess="Toggle" />}
>
<ConstraintAccordionViewHeader
compact={compact}

View File

@ -104,8 +104,9 @@ export const ConstraintAccordionViewHeader = ({
projectId={projectId}
environmentId={environmentId}
hidden={!onEdit}
tooltip="Edit constraint"
>
<Edit titleAccess="edit constraint" />
<Edit />
</PermissionIconButton>
}
/>
@ -116,9 +117,10 @@ export const ConstraintAccordionViewHeader = ({
onClick={onDeleteClick!}
permission={UPDATE_FEATURE_STRATEGY}
projectId={projectId}
tooltip="Delete constraint"
environmentId={environmentId}
>
<Delete titleAccess="delete constraint" />
<Delete />
</PermissionIconButton>
}
/>

View File

@ -6,7 +6,7 @@ export const ConstraintIcon = () => {
return (
<div className={styles.constraintIconContainer}>
<TrackChanges className={styles.constraintIcon} aria-hidden />
<TrackChanges className={styles.constraintIcon} />
</div>
);
};

View File

@ -12,7 +12,7 @@ const DropdownMenu = ({
id,
title,
callback,
icon = <ArrowDropDown />,
icon = <ArrowDropDown titleAccess="Toggle" />,
label,
style,
startIcon,

View File

@ -9,6 +9,8 @@ interface IEnvironmentIcon {
const EnvironmentIcon = ({ enabled, className }: IEnvironmentIcon) => {
const theme = useTheme();
const title = enabled ? 'Environment enabled' : 'Environment disabled';
const container = {
backgroundColor: enabled
? theme.palette.primary.light
@ -31,7 +33,7 @@ const EnvironmentIcon = ({ enabled, className }: IEnvironmentIcon) => {
return (
<div style={container} className={className}>
<Cloud style={icon} />
<Cloud style={icon} titleAccess={title} />
</div>
);
};

View File

@ -1,7 +1,12 @@
import { useStyles } from './FormTemplate.styles';
import MenuBookIcon from '@material-ui/icons/MenuBook';
import Codebox from '../Codebox/Codebox';
import { Collapse, IconButton, useMediaQuery } from '@material-ui/core';
import {
Collapse,
IconButton,
useMediaQuery,
Tooltip,
} from '@material-ui/core';
import { FileCopy, Info } from '@material-ui/icons';
import ConditionallyRender from '../ConditionallyRender';
import Loader from '../Loader/Loader';
@ -95,9 +100,11 @@ const FormTemplate: React.FC<ICreateProps> = ({
>
<h3 className={styles.subtitle}>
API Command{' '}
<IconButton onClick={copyCommand}>
<FileCopy className={styles.icon} />
</IconButton>
<Tooltip title="Copy command">
<IconButton onClick={copyCommand}>
<FileCopy className={styles.icon} />
</IconButton>
</Tooltip>
</h3>
<Codebox text={formatApiCode()} />
</Guidance>
@ -126,12 +133,14 @@ const MobileGuidance = ({
<div className={styles.mobileGuidanceBgContainer}>
<MobileGuidanceBG className={styles.mobileGuidanceBackground} />
</div>
<IconButton
className={styles.mobileGuidanceButton}
onClick={() => setOpen(prev => !prev)}
>
<Info className={styles.infoIcon} />
</IconButton>
<Tooltip title="Toggle help">
<IconButton
className={styles.mobileGuidanceButton}
onClick={() => setOpen(prev => !prev)}
>
<Info className={styles.infoIcon} />
</IconButton>
</Tooltip>
<Collapse in={open} timeout={500}>
<Guidance
description={description}

View File

@ -15,6 +15,9 @@ const PasswordField = ({ ...rest }) => {
e.preventDefault();
};
const IconComponent = showPassword ? Visibility : VisibilityOff;
const iconTitle = 'Toggle password visibility';
return (
<TextField
variant="outlined"
@ -27,11 +30,10 @@ const PasswordField = ({ ...rest }) => {
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
>
{showPassword ? <Visibility /> : <VisibilityOff />}
<IconComponent titleAccess={iconTitle} />
</IconButton>
</InputAdornment>
),

View File

@ -1,15 +1,19 @@
import { Button, ButtonProps, Tooltip } from '@material-ui/core';
import { Button, ButtonProps } from '@material-ui/core';
import { Lock } from '@material-ui/icons';
import AccessContext from 'contexts/AccessContext';
import React, { useContext } from 'react';
import ConditionallyRender from '../ConditionallyRender';
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
import { formatAccessText } from 'utils/formatAccessText';
import { useId } from 'hooks/useId';
export interface IPermissionButtonProps extends ButtonProps {
export interface IPermissionButtonProps extends Omit<ButtonProps, 'title'> {
permission: string | string[];
onClick?: (e: any) => void;
disabled?: boolean;
projectId?: string;
environmentId?: string;
tooltip?: string;
}
const PermissionButton: React.FC<IPermissionButtonProps> = ({
@ -21,9 +25,11 @@ const PermissionButton: React.FC<IPermissionButtonProps> = ({
disabled,
projectId,
environmentId,
tooltip,
...rest
}) => {
const { hasAccess } = useContext(AccessContext);
const id = useId();
let access;
const handleAccess = () => {
@ -53,30 +59,27 @@ const PermissionButton: React.FC<IPermissionButtonProps> = ({
access = handleAccess();
const tooltipText = !access
? "You don't have access to perform this operation"
: '';
return (
<Tooltip title={tooltipText} arrow>
<span>
<TooltipResolver title={formatAccessText(access, tooltip)} arrow>
<span id={id}>
<Button
onClick={onClick}
disabled={disabled || !access}
aria-describedby={id}
variant={variant}
color={color}
{...rest}
endIcon={
<ConditionallyRender
condition={!access}
show={<Lock />}
show={<Lock titleAccess="Locked" />}
/>
}
>
{children}
</Button>
</span>
</Tooltip>
</TooltipResolver>
);
};

View File

@ -1,14 +1,17 @@
import { IconButton, Tooltip, IconButtonProps } from '@material-ui/core';
import { IconButton, IconButtonProps } from '@material-ui/core';
import React, { useContext, ReactNode } from 'react';
import AccessContext from 'contexts/AccessContext';
import { Link } from 'react-router-dom';
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
import { formatAccessText } from 'utils/formatAccessText';
import { useId } from 'hooks/useId';
interface IPermissionIconButtonProps {
permission: string;
projectId?: string;
environmentId?: string;
className?: string;
title?: string;
tooltip?: string;
children?: ReactNode;
disabled?: boolean;
hidden?: boolean;
@ -30,9 +33,11 @@ const PermissionIconButton = ({
projectId,
children,
environmentId,
tooltip,
...rest
}: IButtonProps | ILinkProps) => {
const { hasAccess } = useContext(AccessContext);
const id = useId();
let access;
if (projectId && environmentId) {
@ -43,18 +48,14 @@ const PermissionIconButton = ({
access = hasAccess(permission);
}
const tooltipText = !access
? "You don't have access to perform this operation"
: '';
return (
<Tooltip title={tooltipText} arrow>
<span>
<IconButton disabled={!access} {...rest}>
<TooltipResolver title={formatAccessText(access, tooltip)}>
<span id={id}>
<IconButton {...rest} disabled={!access} aria-labelledby={id}>
{children}
</IconButton>
</span>
</Tooltip>
</TooltipResolver>
);
};

View File

@ -1,6 +1,8 @@
import { Switch, Tooltip, SwitchProps } from '@material-ui/core';
import { Switch, SwitchProps } from '@material-ui/core';
import AccessContext from 'contexts/AccessContext';
import React, { useContext } from 'react';
import { formatAccessText } from 'utils/formatAccessText';
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
interface IPermissionSwitchProps extends SwitchProps {
permission: string;
@ -38,12 +40,8 @@ const PermissionSwitch = React.forwardRef<
access = hasAccess(permission);
}
const tooltipText = !access
? "You don't have access to perform this operation"
: '';
return (
<Tooltip title={tooltipText} arrow>
<TooltipResolver title={formatAccessText(access, tooltip)} arrow>
<span data-loading>
<Switch
onChange={onChange}
@ -53,7 +51,7 @@ const PermissionSwitch = React.forwardRef<
{...rest}
/>
</span>
</Tooltip>
</TooltipResolver>
);
});

View File

@ -53,7 +53,7 @@ export const TableActions = ({ search, onSearch }: ITableActionsProps) => {
</Tooltip>
}
/>
<div className={styles.verticalSeparator}></div>
<div className={styles.verticalSeparator} />
</>
);
};

View File

@ -57,7 +57,6 @@ export const TableSearchField = ({
<Tooltip title="Clear search query" arrow>
<IconButton
size="small"
aria-label="Clear search query"
onClick={() => {
onChange('');
onBlur?.(true);

View File

@ -1,7 +1,7 @@
import { useStyles } from './Toast.styles';
import classnames from 'classnames';
import { useContext } from 'react';
import { IconButton } from '@material-ui/core';
import { IconButton, Tooltip } from '@material-ui/core';
import CheckMarkBadge from 'component/common/CheckmarkBadge/CheckMarkBadge';
import UIContext from 'contexts/UIContext';
import ConditionallyRender from 'component/common/ConditionallyRender';
@ -76,14 +76,15 @@ const Toast = ({ title, text, type, confetti }: IToast) => {
/>
</div>
</div>
<IconButton
color="primary"
onClick={hide}
className={styles.buttonStyle}
>
<Close />
</IconButton>
<Tooltip title="Close">
<IconButton
color="primary"
onClick={hide}
className={styles.buttonStyle}
>
<Close />
</IconButton>
</Tooltip>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
import { Tooltip, TooltipProps } from '@material-ui/core';
interface ITooltipResolverProps extends Omit<TooltipProps, 'title'> {
title: string | undefined;
}
export const TooltipResolver = ({
title,
children,
...rest
}: ITooltipResolverProps) => {
if (!title) {
return children;
}
return (
<Tooltip {...rest} title={title}>
{children}
</Tooltip>
);
};

View File

@ -79,7 +79,6 @@ const ContextList = () => {
show={
<Tooltip title="Edit context field">
<IconButton
aria-label="edit"
onClick={() =>
history.push(`/context/edit/${field.name}`)
}
@ -94,7 +93,6 @@ const ContextList = () => {
show={
<Tooltip title="Delete context field">
<IconButton
aria-label="delete"
onClick={() => {
setName(field.name);
setShowDelDialogue(true);

View File

@ -155,46 +155,38 @@ const EnvironmentListItem = ({
<ConditionallyRender
condition={updatePermission}
show={
<Tooltip title="Drag to reorder">
<div>
<IconButton>
<DragIndicator titleAccess="Drag" />
</IconButton>
</div>
</Tooltip>
<IconButton>
<DragIndicator titleAccess="Drag" cursor="grab" />
</IconButton>
}
/>
<ConditionallyRender
condition={updatePermission}
show={
<Tooltip title={`${tooltipText} environment`}>
<div>
<IconButton
onClick={() => {
setSelectedEnv(env);
setToggleDialog(prev => !prev);
}}
>
<OfflineBolt titleAccess="Toggle" />
</IconButton>
</div>
<IconButton
onClick={() => {
setSelectedEnv(env);
setToggleDialog(prev => !prev);
}}
>
<OfflineBolt />
</IconButton>
</Tooltip>
}
/>
<ConditionallyRender
condition={updatePermission}
show={
<Tooltip title="Update environment">
<div>
<IconButton
disabled={env.protected}
onClick={() => {
history.push(`/environments/${env.name}`);
}}
>
<Edit titleAccess="Edit" />
</IconButton>
</div>
<Tooltip title="Edit environment">
<IconButton
disabled={env.protected}
onClick={() => {
history.push(`/environments/${env.name}`);
}}
>
<Edit />
</IconButton>
</Tooltip>
}
/>
@ -202,17 +194,15 @@ const EnvironmentListItem = ({
condition={hasAccess(DELETE_ENVIRONMENT)}
show={
<Tooltip title="Delete environment">
<div>
<IconButton
disabled={env.protected}
onClick={() => {
setDeldialogue(true);
setSelectedEnv(env);
}}
>
<Delete titleAccess="Delete" />
</IconButton>
</div>
<IconButton
disabled={env.protected}
onClick={() => {
setDeldialogue(true);
setSelectedEnv(env);
}}
>
<Delete />
</IconButton>
</Tooltip>
}
/>

View File

@ -35,7 +35,7 @@ export const CreateFeatureButton = ({
data-testid={NAVIGATE_TO_CREATE_FEATURE}
disabled={!createFeature.access}
>
<Add titleAccess="New" />
<Add />
</IconButton>
</Tooltip>
}

View File

@ -35,7 +35,7 @@ export const FeatureStrategyMenuCard = ({
return (
<Link to={createStrategyPath} className={styles.card}>
<div className={styles.icon}>
<StrategyIcon aria-hidden />
<StrategyIcon />
</div>
<div>
<StringTruncator

View File

@ -69,9 +69,10 @@ export const FeatureStrategyRemove = ({
disabled={disabled}
permission={DELETE_FEATURE_STRATEGY}
data-testid={STRATEGY_FORM_REMOVE_ID}
tooltip="Delete strategy"
type="button"
>
<Delete titleAccess="Delete strategy" />
<Delete />
</PermissionIconButton>
}
elseShow={

View File

@ -145,6 +145,7 @@ const FeatureToggleListItem = ({
!projectExists()
}
onClick={reviveFeature}
tooltip="Revive feature"
>
<Undo />
</PermissionIconButton>

View File

@ -128,14 +128,17 @@ exports[`renders correctly with one feature 1`] = `
className="material-icons MuiIcon-root"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
role="img"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
<title>
Toggle
</title>
</svg>
</span>
</span>
@ -296,14 +299,17 @@ exports[`renders correctly with one feature without permissions 1`] = `
className="material-icons MuiIcon-root"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
role="img"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
<title>
Toggle
</title>
</svg>
</span>
</span>

View File

@ -93,7 +93,7 @@ const FeatureOverviewEnvironment = ({
>
<AccordionSummary
className={styles.accordionHeader}
expandIcon={<ExpandMore />}
expandIcon={<ExpandMore titleAccess="Toggle" />}
>
<div className={styles.header} data-loading>
<div className={styles.headerTitle}>

View File

@ -53,8 +53,9 @@ const FeatureOverviewEnvironmentStrategy = ({
projectId={projectId}
component={Link}
to={editStrategyPath}
tooltip="Edit"
>
<Edit titleAccess="Edit" />
<Edit />
</PermissionIconButton>
<FeatureStrategyRemove
projectId={projectId}

View File

@ -52,7 +52,10 @@ const FeatureOverviewMetaData = () => {
component={Link}
to={`/projects/${projectId}/features/${featureId}/settings`}
>
<Edit className={styles.editIcon} />
<Edit
className={styles.editIcon}
titleAccess="Settings"
/>
</PermissionIconButton>
</div>
</span>
@ -66,6 +69,7 @@ const FeatureOverviewMetaData = () => {
permission={UPDATE_FEATURE}
component={Link}
to={`/projects/${projectId}/features/${featureId}/settings`}
tooltip="Edit description"
>
<Edit className={styles.editIcon} />
</PermissionIconButton>

View File

@ -106,7 +106,9 @@ const FeatureOverviewTags: React.FC<IFeatureOverviewTagsProps> = ({
data-loading
label={t.value}
key={`${t.type}:${t.value}`}
deleteIcon={<Close className={styles.closeIcon} />}
deleteIcon={
<Close className={styles.closeIcon} titleAccess="Remove" />
}
onDelete={
canDeleteTag
? () => {

View File

@ -34,8 +34,9 @@ export const FeatureSettingsInformation = ({
projectId={projectId}
data-loading
onClick={onEdit}
tooltip="Edit"
>
<Edit titleAccess="Edit" />
<Edit />
</PermissionIconButton>
</div>
<Typography>

View File

@ -90,7 +90,10 @@ const FeatureSettingsProjectConfirm = ({
</p>
<div className={styles.iconContainer}>
<div className={styles.errorIconContainer}>
<Error className={styles.check} />
<Error
className={styles.check}
titleAccess="Error"
/>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { Grid, IconButton, TextField } from '@material-ui/core';
import { Grid, IconButton, TextField, Tooltip } from '@material-ui/core';
import { Delete } from '@material-ui/icons';
import { useStyles } from './OverrideConfig.styles';
import { Autocomplete } from '@material-ui/lab';
@ -98,9 +98,11 @@ export const OverrideConfig = ({
/>
</Grid>
<Grid item md={1}>
<IconButton onClick={removeOverride(i)}>
<Delete />
</IconButton>
<Tooltip title="Remove">
<IconButton onClick={removeOverride(i)}>
<Delete />
</IconButton>
</Tooltip>
</Grid>
</Grid>
);

View File

@ -1,4 +1,10 @@
import { Chip, IconButton, TableCell, TableRow } from '@material-ui/core';
import {
Chip,
IconButton,
TableCell,
TableRow,
Tooltip,
} from '@material-ui/core';
import { Delete, Edit } from '@material-ui/icons';
import styles from '../variants.module.scss';
@ -54,24 +60,28 @@ const FeatureVariantListItem = ({
show={
<TableCell className={styles.actions}>
<div className={styles.actionsContainer}>
<IconButton
data-testid={'VARIANT_EDIT_BUTTON'}
onClick={() => editVariant(variant.name)}
>
<Edit />
</IconButton>
<IconButton
data-testid={`VARIANT_DELETE_BUTTON_${variant.name}`}
onClick={e => {
e.stopPropagation();
setDelDialog({
show: true,
name: variant.name,
});
}}
>
<Delete />
</IconButton>
<Tooltip title="Edit variant">
<IconButton
data-testid={'VARIANT_EDIT_BUTTON'}
onClick={() => editVariant(variant.name)}
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip title="Delete variant">
<IconButton
data-testid={`VARIANT_DELETE_BUTTON_${variant.name}`}
onClick={e => {
e.stopPropagation();
setDelDialog({
show: true,
name: variant.name,
});
}}
>
<Delete />
</IconButton>
</Tooltip>
</div>
</TableCell>
}

View File

@ -143,32 +143,36 @@ export const FeatureView = () => {
data-loading
component={Link}
to={`/projects/${projectId}/features/${featureId}/strategies/copy`}
tooltip="Copy feature"
>
<FileCopy titleAccess="Copy" />
<FileCopy />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_FEATURE}
projectId={projectId}
tooltip="Archive feature toggle"
data-loading
onClick={() => setShowDelDialog(true)}
>
<Archive titleAccess="Archive feature toggle" />
<Archive />
</PermissionIconButton>
<PermissionIconButton
onClick={() => setOpenStaleDialog(true)}
permission={UPDATE_FEATURE}
projectId={projectId}
tooltip="Toggle stale status"
data-loading
>
<WatchLater titleAccess="Toggle stale status" />
<WatchLater />
</PermissionIconButton>
<PermissionIconButton
onClick={() => setOpenTagDialog(true)}
permission={UPDATE_FEATURE}
projectId={projectId}
tooltip="Add tag"
data-loading
>
<Label titleAccess="Add tag" />
<Label />
</PermissionIconButton>
</div>
</div>

View File

@ -175,7 +175,7 @@ const StrategyConstraintInputField = ({
</td>
<td className={styles.tableCell}>
<IconButton onClick={removeConstraint}>
<Delete />
<Delete titleAccess="Remove constraint" />
</IconButton>
</td>
</tr>

View File

@ -92,7 +92,10 @@ const StrategyConstraints: React.FC<IStrategyConstraintProps> = ({
<Typography variant="subtitle2">
{'Constraints '}
<Info style={{ fontSize: '0.9rem', color: 'gray' }} />
<Info
style={{ fontSize: '0.9rem', color: 'gray' }}
titleAccess="Help"
/>
</Typography>
</Tooltip>
<table style={{ margin: 0 }}>

View File

@ -1,5 +1,5 @@
import { useContext, useState } from 'react';
import { Button, IconButton } from '@material-ui/core';
import { Button, IconButton, Tooltip } from '@material-ui/core';
import classnames from 'classnames';
import CloseIcon from '@material-ui/icons/Close';
import { ReactComponent as Logo } from 'assets/icons/logoPlain.svg';
@ -73,12 +73,14 @@ export const FeedbackNPS = ({ openUrl }: IFeedbackNPSProps) => {
commonStyles.contentSpacingY
)}
>
<IconButton
className={styles.close}
onClick={() => setShowFeedback(false)}
>
<CloseIcon />
</IconButton>
<Tooltip title="Close">
<IconButton
className={styles.close}
onClick={() => setShowFeedback(false)}
>
<CloseIcon />
</IconButton>
</Tooltip>
<Logo className={styles.logo} />
<ConditionallyRender
condition={answeredNotNow}

View File

@ -66,12 +66,14 @@ const Header = () => {
<ConditionallyRender
condition={smallScreen}
show={
<IconButton
className={styles.drawerButton}
onClick={toggleDrawer}
>
<MenuIcon titleAccess="Menu" />
</IconButton>
<Tooltip title="Menu">
<IconButton
className={styles.drawerButton}
onClick={toggleDrawer}
>
<MenuIcon />
</IconButton>
</Tooltip>
}
elseShow={
<Link
@ -136,26 +138,29 @@ const Header = () => {
>
<MenuBookIcon
className={styles.docsIcon}
titleAccess="Documentation"
/>
</a>
</Tooltip>
<ConditionallyRender
condition={admin}
show={
<IconButton
onClick={e =>
setAnchorEl(e.currentTarget)
}
>
<SettingsIcon
className={styles.docsIcon}
titleAccess="Settings"
/>
</IconButton>
<Tooltip title="Settings">
<IconButton
onClick={e =>
setAnchorEl(
e.currentTarget
)
}
>
<SettingsIcon
className={
styles.docsIcon
}
/>
</IconButton>
</Tooltip>
}
/>
<NavigationMenu
id="admin-navigation"
options={filteredMainRoutes.adminRoutes}
@ -166,7 +171,6 @@ const Header = () => {
</>
}
/>
<UserProfile />
</div>
</Container>

View File

@ -128,6 +128,7 @@ const Project = () => {
permission={UPDATE_PROJECT}
projectId={project?.id}
onClick={() => history.push(`/projects/${id}/edit`)}
tooltip="Edit project"
data-loading
>
<Edit />

View File

@ -56,6 +56,7 @@ const ProjectInfo = ({
className={permissionButtonClass}
data-loading
to={`/projects/${id}/edit`}
tooltip="Edit description"
>
<Edit />
</PermissionIconButton>
@ -82,7 +83,9 @@ const ProjectInfo = ({
elseShow={
<Accordion className={styles.accordion}>
<AccordionSummary
expandIcon={<ExpandMore />}
expandIcon={
<ExpandMore titleAccess="Toggle" />
}
className={styles.accordionBody}
>
Description

View File

@ -71,10 +71,9 @@ export const ProjectAccessListItem = ({
projectId={projectId}
className={styles.iconButton}
edge="end"
aria-label="delete"
title="Remove access"
onClick={() => handleRemoveAccess(user)}
disabled={access.users.length === 1}
tooltip="Remove access"
>
<Delete />
</PermissionIconButton>

View File

@ -65,7 +65,7 @@ export const ProjectCard = ({
return (
<Card className={styles.projectCard} onMouseEnter={onHover}>
<div className={styles.header} data-loading>
<div className={styles.title}>{name}</div>
<h2 className={styles.title}>{name}</h2>
<PermissionIconButton
permission={UPDATE_PROJECT}
@ -73,6 +73,7 @@ export const ProjectCard = ({
className={styles.actionsBtn}
data-loading
onClick={handleClick}
tooltip="Menu"
>
<MoreVertIcon />
</PermissionIconButton>

View File

@ -65,12 +65,12 @@ export const SegmentListItem = ({
push(`/segments/edit/${id}`);
}}
permission={UPDATE_SEGMENT}
tooltip="Edit segment"
>
<Edit titleAccess="Edit segment" />
<Edit />
</PermissionIconButton>
<PermissionIconButton
data-loading
aria-label="Remove segment"
onClick={() => {
setCurrentSegment({
id,
@ -83,6 +83,7 @@ export const SegmentListItem = ({
setDelDialog(true);
}}
permission={ADMIN}
tooltip="Remove segment"
data-testid={`${SEGMENT_DELETE_BTN_ID}_${name}`}
>
<Delete />

View File

@ -72,6 +72,7 @@ export const StrategiesList = () => {
data-testid={ADD_NEW_STRATEGY_ID}
onClick={() => history.push('/strategies/create')}
permission={CREATE_STRATEGY}
tooltip="New strategy"
>
<Add />
</PermissionIconButton>
@ -162,16 +163,13 @@ export const StrategiesList = () => {
};
const reactivateButton = (strategy: ICustomStrategy) => (
<Tooltip title="Reactivate activation strategy">
<div>
<PermissionIconButton
onClick={() => onReactivateStrategy(strategy)}
permission={UPDATE_STRATEGY}
>
<VisibilityOff titleAccess="Reactivate" />
</PermissionIconButton>
</div>
</Tooltip>
<PermissionIconButton
onClick={() => onReactivateStrategy(strategy)}
permission={UPDATE_STRATEGY}
tooltip="Reactivate activation strategy"
>
<VisibilityOff />
</PermissionIconButton>
);
const deprecateButton = (strategy: ICustomStrategy) => (
@ -191,8 +189,9 @@ export const StrategiesList = () => {
<PermissionIconButton
onClick={() => onDeprecateStrategy(strategy)}
permission={UPDATE_STRATEGY}
tooltip="Deprecate strategy"
>
<Visibility titleAccess="Deprecate strategy" />
<Visibility />
</PermissionIconButton>
</div>
}
@ -208,8 +207,9 @@ export const StrategiesList = () => {
history.push(`/strategies/${strategy?.name}/edit`)
}
permission={UPDATE_STRATEGY}
tooltip="Edit strategy"
>
<Edit titleAccess="Edit strategy" />
<Edit />
</PermissionIconButton>
}
elseShow={
@ -231,6 +231,7 @@ export const StrategiesList = () => {
<PermissionIconButton
onClick={() => onDeleteStrategy(strategy)}
permission={DELETE_STRATEGY}
tooltip="Delete strategy"
>
<Delete />
</PermissionIconButton>
@ -239,7 +240,7 @@ export const StrategiesList = () => {
<Tooltip title="You cannot delete a built-in strategy">
<div>
<IconButton disabled>
<Delete />
<Delete titleAccess="Delete strategy" />
</IconButton>
</div>
</Tooltip>

View File

@ -30,17 +30,10 @@ exports[`renders correctly with one strategy 1`] = `
className="makeStyles-headerActions-8"
>
<span
aria-describedby={null}
className=""
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title=""
id="useId-0"
>
<button
aria-describedby="useId-0"
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
data-testid="ADD_NEW_STRATEGY_ID"
disabled={false}
@ -129,15 +122,17 @@ exports[`renders correctly with one strategy 1`] = `
<span
aria-describedby={null}
className=""
id="useId-1"
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title=""
title="Deprecate strategy"
>
<button
aria-labelledby="useId-1"
className="MuiButtonBase-root MuiIconButton-root"
disabled={false}
onBlur={[Function]}
@ -159,17 +154,14 @@ exports[`renders correctly with one strategy 1`] = `
className="MuiIconButton-label"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
role="img"
viewBox="0 0 24 24"
>
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"
/>
<title>
Deprecate strategy
</title>
</svg>
</span>
<span
@ -257,14 +249,17 @@ exports[`renders correctly with one strategy 1`] = `
className="MuiIconButton-label"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
role="img"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
<title>
Delete strategy
</title>
</svg>
</span>
</button>
@ -305,17 +300,10 @@ exports[`renders correctly with one strategy without permissions 1`] = `
className="makeStyles-headerActions-8"
>
<span
aria-describedby={null}
className=""
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title=""
id="useId-2"
>
<button
aria-describedby="useId-2"
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
data-testid="ADD_NEW_STRATEGY_ID"
disabled={false}
@ -404,15 +392,17 @@ exports[`renders correctly with one strategy without permissions 1`] = `
<span
aria-describedby={null}
className=""
id="useId-3"
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title=""
title="Deprecate strategy"
>
<button
aria-labelledby="useId-3"
className="MuiButtonBase-root MuiIconButton-root"
disabled={false}
onBlur={[Function]}
@ -434,17 +424,14 @@ exports[`renders correctly with one strategy without permissions 1`] = `
className="MuiIconButton-label"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
role="img"
viewBox="0 0 24 24"
>
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"
/>
<title>
Deprecate strategy
</title>
</svg>
</span>
<span
@ -532,14 +519,17 @@ exports[`renders correctly with one strategy without permissions 1`] = `
className="MuiIconButton-label"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
role="img"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
<title>
Delete strategy
</title>
</svg>
</span>
</button>

View File

@ -1,4 +1,9 @@
import { Checkbox, FormControlLabel, IconButton } from '@material-ui/core';
import {
Checkbox,
FormControlLabel,
IconButton,
Tooltip,
} from '@material-ui/core';
import { Delete } from '@material-ui/icons';
import { useStyles } from './StrategyParameter.styles';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
@ -74,7 +79,7 @@ export const StrategyParameter = ({
condition={index === 0}
show={
<p className={styles.input}>
The parameters define how the strategy will look like.
The parameters define what the strategy will look like.
</p>
}
/>
@ -88,13 +93,15 @@ export const StrategyParameter = ({
error={Boolean(errors?.[`paramName${index}`])}
errorText={errors?.[`paramName${index}`]}
/>
<IconButton
onClick={() => {
setParams(params.filter((e, i) => i !== index));
}}
>
<Delete titleAccess="Delete" />
</IconButton>
<Tooltip title="Remove parameter">
<IconButton
onClick={() => {
setParams(params.filter((e, i) => i !== index));
}}
>
<Delete />
</IconButton>
</Tooltip>
</div>
<GeneralSelect
label="Type*"

View File

@ -34,18 +34,18 @@ export const StrategyDetails = ({
<ConditionallyRender
condition={required}
show={
<Tooltip title="Required">
<ListItemAvatar>
<Add />
</ListItemAvatar>
</Tooltip>
<ListItemAvatar>
<Tooltip title="Required parameter">
<Add aria-hidden={false} />
</Tooltip>
</ListItemAvatar>
}
elseShow={
<Tooltip title="Optional">
<ListItemAvatar>
<RadioButtonChecked />
</ListItemAvatar>
</Tooltip>
<ListItemAvatar>
<Tooltip title="Optional parameter">
<RadioButtonChecked aria-hidden={false} />
</Tooltip>
</ListItemAvatar>
}
/>
<ListItemText

View File

@ -43,8 +43,9 @@ export const StrategyView = () => {
permission={UPDATE_STRATEGY}
data-loading
onClick={handleEdit}
tooltip="Edit strategy"
>
<Edit titleAccess="Edit strategy" />
<Edit />
</PermissionIconButton>
}
/>

View File

@ -19,30 +19,33 @@ export const TogglesLinkList = ({ toggles }: ITogglesLinkListProps) => (
<List style={{ textAlign: 'left' }} className={styles.truncate}>
<ConditionallyRender
condition={toggles.length > 0}
show={
<>
{toggles.map(({ name, description = '-', enabled }) => (
<ListItem key={name}>
<Tooltip title={enabled ? 'Enabled' : 'Disabled'}>
<ListItemAvatar>
{enabled ? <PlayArrow /> : <Pause />}
</ListItemAvatar>
</Tooltip>
<ListItemText
primary={
<Link
key={name}
to={`/features/view/${name}`}
>
{name}
</Link>
}
secondary={description}
/>
</ListItem>
))}
</>
}
show={toggles.map(({ name, description = '-', enabled }) => (
<ListItem key={name}>
<ListItemAvatar>
<ConditionallyRender
condition={Boolean(enabled)}
show={
<Tooltip title="Enabled">
<PlayArrow aria-hidden={false} />
</Tooltip>
}
elseShow={
<Tooltip title="Disabled">
<Pause aria-hidden={false} />
</Tooltip>
}
/>
</ListItemAvatar>
<ListItemText
primary={
<Link key={name} to={`/features/view/${name}`}>
{name}
</Link>
}
secondary={description}
/>
</ListItem>
))}
/>
</List>
);

View File

@ -69,7 +69,6 @@ export const TagTypeList = () => {
show={
<Tooltip title="Add tag type">
<IconButton
aria-label="add tag type"
onClick={() =>
history.push('/tag-types/create')
}
@ -129,9 +128,10 @@ export const TagTypeList = () => {
<PermissionIconButton
permission={UPDATE_TAG_TYPE}
component={Link}
tooltip="Edit tag type"
to={`/tag-types/edit/${tagType.name}`}
>
<Edit className={styles.icon} titleAccess="Edit tag type" />
<Edit className={styles.icon} />
</PermissionIconButton>
<ConditionallyRender
condition={hasAccess(DELETE_TAG_TYPE)}

View File

@ -14,6 +14,7 @@ import {
NotFoundError,
} from 'utils/apiUtils';
import { formatApiPath } from 'utils/formatPath';
import { ACCESS_DENIED_TEXT } from 'utils/formatAccessText';
type ApiErrorHandler = (
setErrors: Dispatch<SetStateAction<{}>>,
@ -125,8 +126,7 @@ const useAPI = ({
} else {
setErrors(prev => ({
...prev,
unauthorized:
'You are not authorized to perform this operation',
unauthorized: ACCESS_DENIED_TEXT,
}));
}

View File

@ -0,0 +1,40 @@
import { useId } from 'hooks/useId';
import { renderHook } from '@testing-library/react-hooks';
test('useId', () => {
const { result, rerender } = renderHook(() => useId());
rerender();
rerender();
expect(result).toMatchInlineSnapshot(`
Object {
"all": Array [
"useId-0",
"useId-0",
"useId-0",
],
"current": "useId-0",
"error": undefined,
}
`);
});
test('useId prefix', () => {
const { result, rerender } = renderHook(() => useId('prefix'));
rerender();
rerender();
expect(result).toMatchInlineSnapshot(`
Object {
"all": Array [
"prefix-1",
"prefix-1",
"prefix-1",
],
"current": "prefix-1",
"error": undefined,
}
`);
});

View File

@ -0,0 +1,10 @@
import { useMemo } from 'react';
// Generate a globally unique ID that is stable across renders.
export const useId = (prefix = 'useId'): string => {
return useMemo(() => {
return `${prefix}-${counter++}`;
}, [prefix]);
};
let counter = 0;

View File

@ -0,0 +1,11 @@
import { formatAccessText } from 'utils/formatAccessText';
test('formatAccessText with access', () => {
expect(formatAccessText(true)).toEqual(undefined);
expect(formatAccessText(true, 'Foo')).toEqual('Foo');
});
test('formatAccessText without access', () => {
expect(formatAccessText(false)).toEqual('Access denied');
expect(formatAccessText(false, 'Foo')).toEqual(`Foo (Access denied)`);
});

View File

@ -0,0 +1,16 @@
export const ACCESS_DENIED_TEXT = 'Access denied';
export const formatAccessText = (
hasAccess: boolean,
hasAccessText?: string
): string | undefined => {
if (hasAccess) {
return hasAccessText;
}
if (hasAccessText) {
return `${hasAccessText} (${ACCESS_DENIED_TEXT})`;
}
return ACCESS_DENIED_TEXT;
};