1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Feature overview sidepanel UI improvements (#2502)

https://linear.app/unleash/issue/2-423/update-feature-toggle-overview-sidepanel
Misc UI improvements.
This commit is contained in:
Nuno Góis 2022-11-22 15:51:41 +00:00 committed by GitHub
parent 801df6953c
commit 4d5c12dbf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 39 deletions

View File

@ -1,4 +1,4 @@
import { Typography } from '@mui/material'; import { styled, Typography } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import Input from 'component/common/Input/Input'; import Input from 'component/common/Input/Input';
@ -10,6 +10,11 @@ import useTags from 'hooks/api/getters/useTags/useTags';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { ITag } from 'interfaces/tags';
const StyledInput = styled(Input)(() => ({
width: '100%',
}));
interface IAddTagDialogProps { interface IAddTagDialogProps {
open: boolean; open: boolean;
@ -28,7 +33,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const { addTagToFeature, loading } = useFeatureApi(); const { addTagToFeature, loading } = useFeatureApi();
const { refetch } = useTags(featureId); const { tags, refetch } = useTags(featureId);
const [errors, setErrors] = useState({ tagError: '' }); const [errors, setErrors] = useState({ tagError: '' });
const { setToastData } = useToast(); const { setToastData } = useToast();
const [tag, setTag] = useState(DEFAULT_TAG); const [tag, setTag] = useState(DEFAULT_TAG);
@ -39,12 +44,6 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
setTag(DEFAULT_TAG); setTag(DEFAULT_TAG);
}; };
const setValue = (field: string, value: string) => {
const newTag = { ...tag };
newTag[field] = trim(value);
setTag(newTag);
};
const onSubmit = async (evt: React.SyntheticEvent) => { const onSubmit = async (evt: React.SyntheticEvent) => {
evt.preventDefault(); evt.preventDefault();
if (!tag.type) { if (!tag.type) {
@ -68,6 +67,26 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
} }
}; };
const isValueNotEmpty = (name: string) => name.length;
const isTagUnique = (tag: ITag) =>
!tags.some(
({ type, value }) => type === tag.type && value === tag.value
);
const isValid = isValueNotEmpty(tag.value) && isTagUnique(tag);
const onUpdateTag = (key: string, value: string) => {
setErrors({ tagError: '' });
const updatedTag = { ...tag, [key]: trim(value) };
if (!isTagUnique(updatedTag)) {
setErrors({
tagError: 'Tag already exists for this feature toggle.',
});
}
setTag(updatedTag);
};
const formId = 'add-tag-form'; const formId = 'add-tag-form';
return ( return (
@ -78,7 +97,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
primaryButtonText="Add tag" primaryButtonText="Add tag"
title="Add tags to feature toggle" title="Add tags to feature toggle"
onClick={onSubmit} onClick={onSubmit}
disabledPrimaryButton={loading} disabledPrimaryButton={loading || !isValid}
onClose={onCancel} onClose={onCancel}
formId={formId} formId={formId}
> >
@ -92,10 +111,10 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
autoFocus autoFocus
name="type" name="type"
value={tag.type} value={tag.type}
onChange={type => setValue('type', type)} onChange={type => onUpdateTag('type', type)}
/> />
<br /> <br />
<Input <StyledInput
label="Value" label="Value"
name="value" name="value"
placeholder="Your tag" placeholder="Your tag"
@ -103,8 +122,9 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
error={Boolean(errors.tagError)} error={Boolean(errors.tagError)}
errorText={errors.tagError} errorText={errors.tagError}
onChange={e => onChange={e =>
setValue('value', e.target.value) onUpdateTag('value', e.target.value)
} }
required
/> />
</section> </section>
</form> </form>

View File

@ -7,6 +7,8 @@ import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSi
import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags'; import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags';
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
position: 'sticky',
top: theme.spacing(2),
borderRadius: theme.shape.borderRadiusLarge, borderRadius: theme.shape.borderRadiusLarge,
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
display: 'flex', display: 'flex',

View File

@ -20,25 +20,27 @@ const StyledContainer = styled('div')(({ theme }) => ({
}, },
})); }));
const StyledLabel = styled('label')(({ theme }) => ({ const StyledLabel = styled('label')(() => ({
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
cursor: 'pointer', cursor: 'pointer',
})); }));
interface IFeatureOverviewSidePanelEnvironmentSwitchProps { interface IFeatureOverviewSidePanelEnvironmentSwitchProps {
env: IFeatureEnvironment; environment: IFeatureEnvironment;
callback?: () => void; callback?: () => void;
showInfoBox: () => void; showInfoBox: () => void;
children?: React.ReactNode; children?: React.ReactNode;
} }
export const FeatureOverviewSidePanelEnvironmentSwitch = ({ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
env, environment,
callback, callback,
showInfoBox, showInfoBox,
children, children,
}: IFeatureOverviewSidePanelEnvironmentSwitchProps) => { }: IFeatureOverviewSidePanelEnvironmentSwitchProps) => {
const { name, enabled } = environment;
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } = const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
@ -55,11 +57,11 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
const handleToggleEnvironmentOn = async () => { const handleToggleEnvironmentOn = async () => {
try { try {
await toggleFeatureEnvironmentOn(projectId, featureId, env.name); await toggleFeatureEnvironmentOn(projectId, featureId, name);
setToastData({ setToastData({
type: 'success', type: 'success',
title: `Available in ${env.name}`, title: `Available in ${name}`,
text: `${featureId} is now available in ${env.name} based on its defined strategies.`, text: `${featureId} is now available in ${name} based on its defined strategies.`,
}); });
refetchFeature(); refetchFeature();
if (callback) { if (callback) {
@ -79,11 +81,11 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
const handleToggleEnvironmentOff = async () => { const handleToggleEnvironmentOff = async () => {
try { try {
await toggleFeatureEnvironmentOff(projectId, featureId, env.name); await toggleFeatureEnvironmentOff(projectId, featureId, name);
setToastData({ setToastData({
type: 'success', type: 'success',
title: `Unavailable in ${env.name}`, title: `Unavailable in ${name}`,
text: `${featureId} is unavailable in ${env.name} and its strategies will no longer have any effect.`, text: `${featureId} is unavailable in ${name} and its strategies will no longer have any effect.`,
}); });
refetchFeature(); refetchFeature();
if (callback) { if (callback) {
@ -95,12 +97,12 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
}; };
const toggleEnvironment = async (e: React.ChangeEvent) => { const toggleEnvironment = async (e: React.ChangeEvent) => {
if (isChangeRequestConfigured(env.name)) { if (isChangeRequestConfigured(name)) {
e.preventDefault(); e.preventDefault();
onChangeRequestToggle(featureId, env.name, !env.enabled); onChangeRequestToggle(featureId, name, !enabled);
return; return;
} }
if (env.enabled) { if (enabled) {
await handleToggleEnvironmentOff(); await handleToggleEnvironmentOff();
return; return;
} }
@ -110,9 +112,9 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
const defaultContent = ( const defaultContent = (
<> <>
{' '} {' '}
<span data-loading>{env.enabled ? 'enabled' : 'disabled'} in</span> <span data-loading>{enabled ? 'enabled' : 'disabled'} in</span>
&nbsp; &nbsp;
<StringTruncator text={env.name} maxWidth="120" maxLength={15} /> <StringTruncator text={name} maxWidth="120" maxLength={15} />
</> </>
); );
@ -120,11 +122,16 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
<StyledContainer> <StyledContainer>
<StyledLabel> <StyledLabel>
<PermissionSwitch <PermissionSwitch
tooltip={
enabled
? `Disable feature in ${name}`
: `Enable feature in ${name}`
}
permission={UPDATE_FEATURE_ENVIRONMENT} permission={UPDATE_FEATURE_ENVIRONMENT}
projectId={projectId} projectId={projectId}
checked={env.enabled} checked={enabled}
onChange={toggleEnvironment} onChange={toggleEnvironment}
environmentId={env.name} environmentId={name}
/> />
{children ?? defaultContent} {children ?? defaultContent}
</StyledLabel> </StyledLabel>

View File

@ -2,7 +2,7 @@ import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDia
import { IFeatureToggle } from 'interfaces/featureToggle'; import { IFeatureToggle } from 'interfaces/featureToggle';
import { useState } from 'react'; import { useState } from 'react';
import { FeatureOverviewSidePanelEnvironmentSwitch } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch'; import { FeatureOverviewSidePanelEnvironmentSwitch } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch';
import { Link, styled } from '@mui/material'; import { Link, styled, Tooltip } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
@ -55,22 +55,24 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
const variantsLink = variants.length > 0 && ( const variantsLink = variants.length > 0 && (
<> <>
{' - '} {' - '}
<StyledLink <Tooltip title="View variants" arrow describeChild>
component={RouterLink} <StyledLink
to={`/projects/${feature.project}/features/${feature.name}/variants`} component={RouterLink}
underline="hover" to={`/projects/${feature.project}/features/${feature.name}/variants`}
> underline="hover"
{variants.length === 1 >
? '1 variant' {variants.length === 1
: `${variants.length} variants`} ? '1 variant'
</StyledLink> : `${variants.length} variants`}
</StyledLink>
</Tooltip>
</> </>
); );
return ( return (
<FeatureOverviewSidePanelEnvironmentSwitch <FeatureOverviewSidePanelEnvironmentSwitch
key={environment.name} key={environment.name}
env={environment} environment={environment}
showInfoBox={() => { showInfoBox={() => {
setEnvironmentName(environment.name); setEnvironmentName(environment.name);
setShowInfoBox(true); setShowInfoBox(true);