1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

fix: refine project actions form (#6242)

https://linear.app/unleash/issue/2-1934/refine-the-actions-form-uiux

First effort in refining the project actions form to look slightly more
like the design, including some refactors.


![image](https://github.com/Unleash/unleash/assets/14320932/ab6e11b4-b3b4-4c58-8bd1-9fcc9cb7014b)
This commit is contained in:
Nuno Góis 2024-02-15 08:28:20 +00:00 committed by GitHub
parent 6a9f80c554
commit 7a699cf68c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 291 additions and 243 deletions

View File

@ -1,58 +0,0 @@
import { Box, styled } from '@mui/material';
export const StyledInnerBox = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
padding: theme.spacing(2),
borderRadius: `${theme.shape.borderRadiusMedium}px`,
}));
export const StyledInnerBoxHeader = styled('div')(({ theme }) => ({
marginLeft: 'auto',
whiteSpace: 'nowrap',
[theme.breakpoints.down('sm')]: {
display: 'none',
},
}));
// row for inner containers
export const StyledRow = styled('div')({
display: 'flex',
flexDirection: 'row',
width: '100%',
});
export const StyledCol = styled('div')({
flex: 1,
margin: '0 4px',
});
const StyledBoxContent = styled('div')(({ theme }) => ({
padding: theme.spacing(0.75, 1),
color: theme.palette.text.primary,
fontSize: theme.fontSizes.smallerBody,
backgroundColor: theme.palette.seen.primary,
borderRadius: theme.shape.borderRadius,
position: 'absolute',
zIndex: theme.zIndex.fab,
top: '50%',
left: theme.spacing(2),
transform: 'translateY(-50%)',
lineHeight: 1,
}));
export const BoxSeparator: React.FC = ({ children }) => {
return (
<Box
sx={{
height: 1.5,
position: 'relative',
width: '100%',
}}
>
<StyledBoxContent>{children}</StyledBoxContent>
</Box>
);
};

View File

@ -1,19 +1,22 @@
import { IconButton, Tooltip } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Fragment } from 'react';
import { IconButton, Tooltip, styled } from '@mui/material';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { Delete } from '@mui/icons-material';
import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
import {
BoxSeparator,
StyledCol,
StyledInnerBoxHeader,
StyledRow,
StyledInnerBox,
} from './InnerContainerBox';
import { ActionsActionState } from './useProjectActionsForm';
import { ProjectActionsFormItem } from './ProjectActionsFormItem';
const StyledItemRow = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
width: '100%',
}));
const StyledFieldContainer = styled('div')({
flex: 1,
});
export const ProjectActionsActionItem = ({
action,
@ -30,92 +33,90 @@ export const ProjectActionsActionItem = ({
const projectId = useRequiredPathParam('projectId');
const environments = useProjectEnvironments(projectId);
const { features } = useFeatureSearch({ project: `IS:${projectId}` });
const header = (
<>
<span>Action {index + 1}</span>
<div>
<Tooltip title='Delete action' arrow>
<IconButton onClick={onDelete}>
<Delete />
</IconButton>
</Tooltip>
</div>
</>
);
return (
<Fragment>
<ConditionallyRender
condition={index > 0}
show={<BoxSeparator>THEN</BoxSeparator>}
/>
<StyledInnerBox>
<StyledRow>
<span>Action {index + 1}</span>
<StyledInnerBoxHeader>
<Tooltip title='Delete action' arrow>
<IconButton onClick={onDelete}>
<Delete />
</IconButton>
</Tooltip>
</StyledInnerBoxHeader>
</StyledRow>
<StyledRow>
<StyledCol>
<GeneralSelect
label='Action'
name='action'
options={[
{
label: 'Enable flag',
key: 'TOGGLE_FEATURE_ON',
<ProjectActionsFormItem index={index} header={header} separator='THEN'>
<StyledItemRow>
<StyledFieldContainer>
<GeneralSelect
label='Action'
name='action'
options={[
{
label: 'Enable flag',
key: 'TOGGLE_FEATURE_ON',
},
{
label: 'Disable flag',
key: 'TOGGLE_FEATURE_OFF',
},
]}
value={actionName}
onChange={(selected) =>
stateChanged({
...action,
action: selected,
})
}
fullWidth
/>
</StyledFieldContainer>
<StyledFieldContainer>
<GeneralSelect
label='Environment'
name='environment'
options={environments.environments.map((env) => ({
label: env.name,
key: env.name,
}))}
value={action.executionParams.environment as string}
onChange={(selected) =>
stateChanged({
...action,
executionParams: {
...action.executionParams,
environment: selected,
},
{
label: 'Disable flag',
key: 'TOGGLE_FEATURE_OFF',
})
}
fullWidth
/>
</StyledFieldContainer>
<StyledFieldContainer>
<GeneralSelect
label='Flag name'
name='flag'
options={features.map((feature) => ({
label: feature.name,
key: feature.name,
}))}
value={action.executionParams.featureName as string}
onChange={(selected) =>
stateChanged({
...action,
executionParams: {
...action.executionParams,
featureName: selected,
},
]}
value={actionName}
onChange={(selected) =>
stateChanged({
...action,
action: selected,
})
}
fullWidth
/>
</StyledCol>
<StyledCol>
<GeneralSelect
label='Environment'
name='environment'
options={environments.environments.map((env) => ({
label: env.name,
key: env.name,
}))}
value={action.executionParams.environment as string}
onChange={(selected) =>
stateChanged({
...action,
executionParams: {
...action.executionParams,
environment: selected,
},
})
}
fullWidth
/>
</StyledCol>
<StyledCol>
<GeneralSelect
label='Flag name'
name='flag'
options={features.map((feature) => ({
label: feature.name,
key: feature.name,
}))}
value={action.executionParams.featureName as string}
onChange={(selected) =>
stateChanged({
...action,
executionParams: {
...action.executionParams,
featureName: selected,
},
})
}
fullWidth
/>
</StyledCol>
</StyledRow>
</StyledInnerBox>
</Fragment>
})
}
fullWidth
/>
</StyledFieldContainer>
</StyledItemRow>
</ProjectActionsFormItem>
);
};

View File

@ -1,23 +1,20 @@
import { Badge, IconButton, Tooltip, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ActionsFilterState } from './useProjectActionsForm';
import { Fragment } from 'react';
import { Delete } from '@mui/icons-material';
import Input from 'component/common/Input/Input';
import {
BoxSeparator,
StyledInnerBoxHeader,
StyledRow,
StyledInnerBox,
} from './InnerContainerBox';
import { ProjectActionsFormItem } from './ProjectActionsFormItem';
const StyledInput = styled(Input)(() => ({
const StyledInputContainer = styled('div')({
flex: 1,
});
const StyledInput = styled(Input)({
width: '100%',
}));
});
const StyledBadge = styled(Badge)(({ theme }) => ({
color: 'primary',
margin: theme.spacing(1),
margin: theme.spacing(0, 1),
fontSize: theme.fontSizes.mainHeader,
}));
export const ProjectActionsFilterItem = ({
@ -32,47 +29,47 @@ export const ProjectActionsFilterItem = ({
onDelete: () => void;
}) => {
const { parameter, value } = filter;
const header = (
<>
<span>Filter {index + 1}</span>
<div>
<Tooltip title='Delete filter' arrow>
<IconButton onClick={onDelete}>
<Delete />
</IconButton>
</Tooltip>
</div>
</>
);
return (
<Fragment>
<ConditionallyRender
condition={index > 0}
show={<BoxSeparator>AND</BoxSeparator>}
/>
<StyledInnerBox>
<StyledRow>
<span>Filter {index + 1}</span>
<StyledInnerBoxHeader>
<Tooltip title='Delete filter' arrow>
<IconButton type='button' onClick={onDelete}>
<Delete />
</IconButton>
</Tooltip>
</StyledInnerBoxHeader>
</StyledRow>
<StyledRow>
<StyledInput
label='Parameter'
value={parameter}
onChange={(e) =>
stateChanged({
...filter,
parameter: e.target.value,
})
}
/>
<StyledBadge>=</StyledBadge>
<StyledInput
label='Value'
value={value}
onChange={(e) =>
stateChanged({
...filter,
value: e.target.value,
})
}
/>
</StyledRow>
</StyledInnerBox>
</Fragment>
<ProjectActionsFormItem index={index} header={header}>
<StyledInputContainer>
<StyledInput
label='Parameter'
value={parameter}
onChange={(e) =>
stateChanged({
...filter,
parameter: e.target.value,
})
}
/>
</StyledInputContainer>
<StyledBadge>=</StyledBadge>
<StyledInputContainer>
<StyledInput
label='Value'
value={value}
onChange={(e) =>
stateChanged({
...filter,
value: e.target.value,
})
}
/>
</StyledInputContainer>
</ProjectActionsFormItem>
);
};

View File

@ -1,7 +1,6 @@
import { Alert, Box, Button, Link, styled } from '@mui/material';
import { Alert, Button, Divider, Link, styled } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import Input from 'component/common/Input/Input';
import { Badge } from 'component/common/Badge/Badge';
import { FormSwitch } from 'component/common/FormSwitch/FormSwitch';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
@ -16,9 +15,9 @@ import { useMemo } from 'react';
import GeneralSelect, {} from 'component/common/GeneralSelect/GeneralSelect';
import { Add } from '@mui/icons-material';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { StyledRow } from './InnerContainerBox';
import { ProjectActionsActionItem } from './ProjectActionsActionItem';
import { ProjectActionsFilterItem } from './ProjectActionsFilterItem';
import { ProjectActionsFormStep } from './ProjectActionsFormStep';
const StyledServiceAccountAlert = styled(Alert)(({ theme }) => ({
marginBottom: theme.spacing(4),
@ -44,27 +43,20 @@ const StyledInput = styled(Input)(() => ({
width: '100%',
}));
const StyledBadge = styled(Badge)(({ theme }) => ({
color: 'primary',
margin: 'auto',
marginBottom: theme.spacing(1.5),
}));
const StyledBox = styled(Box)(({ theme }) => ({
const StyledSecondaryDescription = styled('p')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.elevation1,
marginTop: theme.spacing(2),
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadiusMedium,
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallBody,
}));
const Step = ({ name, children }: any) => (
<StyledBox>
<StyledBadge color='secondary'>{name}</StyledBadge>
{children}
</StyledBox>
);
const StyledButtonContainer = styled('div')(({ theme }) => ({
marginTop: theme.spacing(2),
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
margin: theme.spacing(3, 0),
marginBottom: theme.spacing(2),
}));
interface IProjectActionsFormProps {
enabled: boolean;
@ -101,6 +93,7 @@ export const ProjectActionsForm = ({
validateName,
validated,
}: IProjectActionsFormProps) => {
const projectId = useRequiredPathParam('projectId');
const { serviceAccounts, loading: serviceAccountsLoading } =
useServiceAccounts();
const { incomingWebhooks, loading: incomingWebhooksLoading } =
@ -177,7 +170,7 @@ export const ProjectActionsForm = ({
}, [serviceAccountsLoading, serviceAccounts]);
const showErrors = validated && Object.values(errors).some(Boolean);
const projectId = useRequiredPathParam('projectId');
return (
<div>
<ConditionallyRender
@ -218,7 +211,7 @@ export const ProjectActionsForm = ({
autoComplete='off'
/>
<Step name='Trigger'>
<ProjectActionsFormStep name='Trigger'>
<StyledInputDescription>
Create incoming webhooks from&nbsp;
<RouterLink to='/integrations/incoming-webhooks'>
@ -235,9 +228,13 @@ export const ProjectActionsForm = ({
setSourceId(parseInt(v));
}}
/>
</Step>
</ProjectActionsFormStep>
<Step name='When this'>
<ProjectActionsFormStep name='When this' verticalConnector>
<StyledSecondaryDescription>
If no filters are defined then the action will be triggered
every time the incoming webhook is called.
</StyledSecondaryDescription>
{filters.map((filter, index) => (
<ProjectActionsFilterItem
key={filter.id}
@ -251,11 +248,8 @@ export const ProjectActionsForm = ({
}
/>
))}
<hr />
<StyledRow>
<StyledButtonContainer>
<Button
type='button'
startIcon={<Add />}
onClick={addFilter}
variant='outlined'
@ -263,10 +257,10 @@ export const ProjectActionsForm = ({
>
Add filter
</Button>
</StyledRow>
</Step>
</StyledButtonContainer>
</ProjectActionsFormStep>
<Step name='Do these action(s)'>
<ProjectActionsFormStep name='Do these action(s)' verticalConnector>
<StyledInputDescription>
Create service accounts from&nbsp;
<RouterLink to='/admin/service-accounts'>
@ -283,7 +277,7 @@ export const ProjectActionsForm = ({
setActorId(parseInt(v));
}}
/>
<hr />
<StyledDivider />
{actions.map((action, index) => (
<ProjectActionsActionItem
index={index}
@ -297,10 +291,8 @@ export const ProjectActionsForm = ({
}
/>
))}
<hr />
<StyledRow>
<StyledButtonContainer>
<Button
type='button'
startIcon={<Add />}
onClick={() => addAction(projectId)}
variant='outlined'
@ -308,8 +300,8 @@ export const ProjectActionsForm = ({
>
Add action
</Button>
</StyledRow>
</Step>
</StyledButtonContainer>
</ProjectActionsFormStep>
<ConditionallyRender
condition={showErrors}

View File

@ -0,0 +1,67 @@
import { styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ReactNode } from 'react';
const StyledItem = styled('div')(({ theme }) => ({
marginTop: theme.spacing(1),
position: 'relative',
}));
const StyledItemSeparator = styled('div')(({ theme }) => ({
padding: theme.spacing(0.75, 1),
fontSize: theme.fontSizes.smallerBody,
backgroundColor: theme.palette.seen.primary,
borderRadius: theme.shape.borderRadius,
position: 'absolute',
zIndex: theme.zIndex.fab,
top: theme.spacing(-2),
left: theme.spacing(2),
lineHeight: 1,
}));
const StyledInnerBox = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
padding: theme.spacing(2),
paddingTop: theme.spacing(1),
borderRadius: theme.shape.borderRadiusMedium,
}));
const StyledRow = styled('div')({
display: 'flex',
alignItems: 'center',
});
const StyledHeaderRow = styled(StyledRow)(({ theme }) => ({
justifyContent: 'space-between',
marginBottom: theme.spacing(1),
}));
interface IProjectActionsFormItemProps {
index: number;
header: ReactNode;
separator?: string;
children: ReactNode;
}
export const ProjectActionsFormItem = ({
index,
header,
separator = 'AND',
children,
}: IProjectActionsFormItemProps) => {
return (
<StyledItem>
<ConditionallyRender
condition={index > 0}
show={<StyledItemSeparator>{separator}</StyledItemSeparator>}
/>
<StyledInnerBox>
<StyledHeaderRow>{header}</StyledHeaderRow>
<StyledRow>{children}</StyledRow>
</StyledInnerBox>
</StyledItem>
);
};

View File

@ -0,0 +1,49 @@
import { Box, Divider, styled } from '@mui/material';
import { Badge } from 'component/common/Badge/Badge';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ReactNode } from 'react';
const StyledBadge = styled(Badge)(({ theme }) => ({
margin: 'auto',
marginBottom: theme.spacing(2),
}));
const StyledBox = styled(Box, {
shouldForwardProp: (prop) => prop !== 'verticalConnector',
})<{ verticalConnector?: boolean }>(({ theme, verticalConnector }) => ({
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.elevation1,
marginTop: verticalConnector ? 0 : theme.spacing(3),
padding: theme.spacing(3),
borderRadius: theme.shape.borderRadiusMedium,
}));
const StyledVerticalConnector = styled(Divider)(({ theme }) => ({
margin: 'auto',
width: 1,
height: theme.spacing(3),
}));
interface IProjectActionsFormStepProps {
name: string;
verticalConnector?: boolean;
children: ReactNode;
}
export const ProjectActionsFormStep = ({
name,
verticalConnector,
children,
}: IProjectActionsFormStepProps) => (
<>
<ConditionallyRender
condition={Boolean(verticalConnector)}
show={<StyledVerticalConnector orientation='vertical' />}
/>
<StyledBox verticalConnector={verticalConnector}>
<StyledBadge color='secondary'>{name}</StyledBadge>
{children}
</StyledBox>
</>
);