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:
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,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);
|
||||||
|
Loading…
Reference in New Issue
Block a user