From 4d5c12dbf7768ee4ddd714893022df1104214bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 22 Nov 2022 15:51:41 +0000 Subject: [PATCH] Feature overview sidepanel UI improvements (#2502) https://linear.app/unleash/issue/2-423/update-feature-toggle-overview-sidepanel Misc UI improvements. --- .../AddTagDialog/AddTagDialog.tsx | 44 ++++++++++++++----- .../FeatureOverviewSidePanel.tsx | 2 + ...tureOverviewSidePanelEnvironmentSwitch.tsx | 39 +++++++++------- ...reOverviewSidePanelEnvironmentSwitches.tsx | 24 +++++----- 4 files changed, 70 insertions(+), 39 deletions(-) diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx index a11ed40a90..c549bc62ca 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx @@ -1,4 +1,4 @@ -import { Typography } from '@mui/material'; +import { styled, Typography } from '@mui/material'; import React, { useState } from 'react'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; 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 { formatUnknownError } from 'utils/formatUnknownError'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import { ITag } from 'interfaces/tags'; + +const StyledInput = styled(Input)(() => ({ + width: '100%', +})); interface IAddTagDialogProps { open: boolean; @@ -28,7 +33,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => { const { classes: styles } = useStyles(); const featureId = useRequiredPathParam('featureId'); const { addTagToFeature, loading } = useFeatureApi(); - const { refetch } = useTags(featureId); + const { tags, refetch } = useTags(featureId); const [errors, setErrors] = useState({ tagError: '' }); const { setToastData } = useToast(); const [tag, setTag] = useState(DEFAULT_TAG); @@ -39,12 +44,6 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => { setTag(DEFAULT_TAG); }; - const setValue = (field: string, value: string) => { - const newTag = { ...tag }; - newTag[field] = trim(value); - setTag(newTag); - }; - const onSubmit = async (evt: React.SyntheticEvent) => { evt.preventDefault(); 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'; return ( @@ -78,7 +97,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => { primaryButtonText="Add tag" title="Add tags to feature toggle" onClick={onSubmit} - disabledPrimaryButton={loading} + disabledPrimaryButton={loading || !isValid} onClose={onCancel} formId={formId} > @@ -92,10 +111,10 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => { autoFocus name="type" value={tag.type} - onChange={type => setValue('type', type)} + onChange={type => onUpdateTag('type', type)} />
- { error={Boolean(errors.tagError)} errorText={errors.tagError} onChange={e => - setValue('value', e.target.value) + onUpdateTag('value', e.target.value) } + required /> diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx index 5ecbc84cc4..7729621819 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx @@ -7,6 +7,8 @@ import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSi import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags'; const StyledContainer = styled('div')(({ theme }) => ({ + position: 'sticky', + top: theme.spacing(2), borderRadius: theme.shape.borderRadiusLarge, backgroundColor: theme.palette.background.paper, display: 'flex', diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx index 0b6e3e3f97..8ba0435d6a 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx @@ -20,25 +20,27 @@ const StyledContainer = styled('div')(({ theme }) => ({ }, })); -const StyledLabel = styled('label')(({ theme }) => ({ +const StyledLabel = styled('label')(() => ({ display: 'inline-flex', alignItems: 'center', cursor: 'pointer', })); interface IFeatureOverviewSidePanelEnvironmentSwitchProps { - env: IFeatureEnvironment; + environment: IFeatureEnvironment; callback?: () => void; showInfoBox: () => void; children?: React.ReactNode; } export const FeatureOverviewSidePanelEnvironmentSwitch = ({ - env, + environment, callback, showInfoBox, children, }: IFeatureOverviewSidePanelEnvironmentSwitchProps) => { + const { name, enabled } = environment; + const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } = @@ -55,11 +57,11 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({ const handleToggleEnvironmentOn = async () => { try { - await toggleFeatureEnvironmentOn(projectId, featureId, env.name); + await toggleFeatureEnvironmentOn(projectId, featureId, name); setToastData({ type: 'success', - title: `Available in ${env.name}`, - text: `${featureId} is now available in ${env.name} based on its defined strategies.`, + title: `Available in ${name}`, + text: `${featureId} is now available in ${name} based on its defined strategies.`, }); refetchFeature(); if (callback) { @@ -79,11 +81,11 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({ const handleToggleEnvironmentOff = async () => { try { - await toggleFeatureEnvironmentOff(projectId, featureId, env.name); + await toggleFeatureEnvironmentOff(projectId, featureId, name); setToastData({ type: 'success', - title: `Unavailable in ${env.name}`, - text: `${featureId} is unavailable in ${env.name} and its strategies will no longer have any effect.`, + title: `Unavailable in ${name}`, + text: `${featureId} is unavailable in ${name} and its strategies will no longer have any effect.`, }); refetchFeature(); if (callback) { @@ -95,12 +97,12 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({ }; const toggleEnvironment = async (e: React.ChangeEvent) => { - if (isChangeRequestConfigured(env.name)) { + if (isChangeRequestConfigured(name)) { e.preventDefault(); - onChangeRequestToggle(featureId, env.name, !env.enabled); + onChangeRequestToggle(featureId, name, !enabled); return; } - if (env.enabled) { + if (enabled) { await handleToggleEnvironmentOff(); return; } @@ -110,9 +112,9 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({ const defaultContent = ( <> {' '} - {env.enabled ? 'enabled' : 'disabled'} in + {enabled ? 'enabled' : 'disabled'} in   - + ); @@ -120,11 +122,16 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({ {children ?? defaultContent} diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx index b2531aa6fa..d08fcc7cc3 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx @@ -2,7 +2,7 @@ import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDia import { IFeatureToggle } from 'interfaces/featureToggle'; import { useState } from 'react'; 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'; const StyledContainer = styled('div')(({ theme }) => ({ @@ -55,22 +55,24 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({ const variantsLink = variants.length > 0 && ( <> {' - '} - - {variants.length === 1 - ? '1 variant' - : `${variants.length} variants`} - + + + {variants.length === 1 + ? '1 variant' + : `${variants.length} variants`} + + ); return ( { setEnvironmentName(environment.name); setShowInfoBox(true);