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:
parent
6a9f80c554
commit
7a699cf68c
@ -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>
|
||||
);
|
||||
};
|
@ -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,25 +33,24 @@ export const ProjectActionsActionItem = ({
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const environments = useProjectEnvironments(projectId);
|
||||
const { features } = useFeatureSearch({ project: `IS:${projectId}` });
|
||||
return (
|
||||
<Fragment>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<BoxSeparator>THEN</BoxSeparator>}
|
||||
/>
|
||||
<StyledInnerBox>
|
||||
<StyledRow>
|
||||
|
||||
const header = (
|
||||
<>
|
||||
<span>Action {index + 1}</span>
|
||||
<StyledInnerBoxHeader>
|
||||
<div>
|
||||
<Tooltip title='Delete action' arrow>
|
||||
<IconButton onClick={onDelete}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</StyledInnerBoxHeader>
|
||||
</StyledRow>
|
||||
<StyledRow>
|
||||
<StyledCol>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ProjectActionsFormItem index={index} header={header} separator='THEN'>
|
||||
<StyledItemRow>
|
||||
<StyledFieldContainer>
|
||||
<GeneralSelect
|
||||
label='Action'
|
||||
name='action'
|
||||
@ -71,8 +73,8 @@ export const ProjectActionsActionItem = ({
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledCol>
|
||||
<StyledCol>
|
||||
</StyledFieldContainer>
|
||||
<StyledFieldContainer>
|
||||
<GeneralSelect
|
||||
label='Environment'
|
||||
name='environment'
|
||||
@ -92,8 +94,8 @@ export const ProjectActionsActionItem = ({
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledCol>
|
||||
<StyledCol>
|
||||
</StyledFieldContainer>
|
||||
<StyledFieldContainer>
|
||||
<GeneralSelect
|
||||
label='Flag name'
|
||||
name='flag'
|
||||
@ -113,9 +115,8 @@ export const ProjectActionsActionItem = ({
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledCol>
|
||||
</StyledRow>
|
||||
</StyledInnerBox>
|
||||
</Fragment>
|
||||
</StyledFieldContainer>
|
||||
</StyledItemRow>
|
||||
</ProjectActionsFormItem>
|
||||
);
|
||||
};
|
||||
|
@ -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,24 +29,23 @@ export const ProjectActionsFilterItem = ({
|
||||
onDelete: () => void;
|
||||
}) => {
|
||||
const { parameter, value } = filter;
|
||||
return (
|
||||
<Fragment>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<BoxSeparator>AND</BoxSeparator>}
|
||||
/>
|
||||
<StyledInnerBox>
|
||||
<StyledRow>
|
||||
|
||||
const header = (
|
||||
<>
|
||||
<span>Filter {index + 1}</span>
|
||||
<StyledInnerBoxHeader>
|
||||
<div>
|
||||
<Tooltip title='Delete filter' arrow>
|
||||
<IconButton type='button' onClick={onDelete}>
|
||||
<IconButton onClick={onDelete}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</StyledInnerBoxHeader>
|
||||
</StyledRow>
|
||||
<StyledRow>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ProjectActionsFormItem index={index} header={header}>
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
label='Parameter'
|
||||
value={parameter}
|
||||
@ -60,7 +56,9 @@ export const ProjectActionsFilterItem = ({
|
||||
})
|
||||
}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
<StyledBadge>=</StyledBadge>
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
label='Value'
|
||||
value={value}
|
||||
@ -71,8 +69,7 @@ export const ProjectActionsFilterItem = ({
|
||||
})
|
||||
}
|
||||
/>
|
||||
</StyledRow>
|
||||
</StyledInnerBox>
|
||||
</Fragment>
|
||||
</StyledInputContainer>
|
||||
</ProjectActionsFormItem>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
||||
<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
|
||||
<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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
Loading…
Reference in New Issue
Block a user