mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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:
		
							parent
							
								
									801df6953c
								
							
						
					
					
						commit
						4d5c12dbf7
					
				@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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',
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
             
 | 
					             
 | 
				
			||||||
            <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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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,6 +55,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
 | 
				
			|||||||
                const variantsLink = variants.length > 0 && (
 | 
					                const variantsLink = variants.length > 0 && (
 | 
				
			||||||
                    <>
 | 
					                    <>
 | 
				
			||||||
                        {' - '}
 | 
					                        {' - '}
 | 
				
			||||||
 | 
					                        <Tooltip title="View variants" arrow describeChild>
 | 
				
			||||||
                            <StyledLink
 | 
					                            <StyledLink
 | 
				
			||||||
                                component={RouterLink}
 | 
					                                component={RouterLink}
 | 
				
			||||||
                                to={`/projects/${feature.project}/features/${feature.name}/variants`}
 | 
					                                to={`/projects/${feature.project}/features/${feature.name}/variants`}
 | 
				
			||||||
@ -64,13 +65,14 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
 | 
				
			|||||||
                                    ? '1 variant'
 | 
					                                    ? '1 variant'
 | 
				
			||||||
                                    : `${variants.length} variants`}
 | 
					                                    : `${variants.length} variants`}
 | 
				
			||||||
                            </StyledLink>
 | 
					                            </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);
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user