mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-02 01:17:58 +02:00
chore: new actions table UI (#6435)
https://linear.app/unleash/issue/2-1995/ui-feature-rename-adapt-the-actions-ui https://linear.app/unleash/issue/2-1988/nice-to-have-last-action-status-on-the-action-sets-table Brings the project actions table UI up to par with the new designs, implementing some features that were discussed with UX.   
This commit is contained in:
parent
454f44dec5
commit
86a795e87c
@ -3,6 +3,13 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
|||||||
import { IActionSet } from 'interfaces/action';
|
import { IActionSet } from 'interfaces/action';
|
||||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||||
|
import { ProjectActionsLastEvent } from './ProjectActionsLastEvent';
|
||||||
|
|
||||||
|
const StyledCell = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
const StyledActionItems = styled('div')(({ theme }) => ({
|
const StyledActionItems = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -34,7 +41,8 @@ export const ProjectActionsActionsCell = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextCell>
|
<StyledCell>
|
||||||
|
<ProjectActionsLastEvent action={action} />
|
||||||
<TooltipLink
|
<TooltipLink
|
||||||
tooltip={
|
tooltip={
|
||||||
<StyledActionItems>
|
<StyledActionItems>
|
||||||
@ -60,6 +68,6 @@ export const ProjectActionsActionsCell = ({
|
|||||||
? '1 action'
|
? '1 action'
|
||||||
: `${actions.length} actions`}
|
: `${actions.length} actions`}
|
||||||
</TooltipLink>
|
</TooltipLink>
|
||||||
</TextCell>
|
</StyledCell>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -0,0 +1,59 @@
|
|||||||
|
import { IActionSet } from 'interfaces/action';
|
||||||
|
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||||
|
import { useActionEvents } from 'hooks/api/getters/useActionEvents/useActionEvents';
|
||||||
|
import { ProjectActionsEventsDetails } from '../ProjectActionsEventsModal/ProjectActionsEventsDetails/ProjectActionsEventsDetails';
|
||||||
|
import { CircularProgress, styled } from '@mui/material';
|
||||||
|
import { CheckCircle, Error as ErrorIcon } from '@mui/icons-material';
|
||||||
|
|
||||||
|
const StyledTooltipLink = styled(TooltipLink)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledSuccessIcon = styled(CheckCircle)(({ theme }) => ({
|
||||||
|
color: theme.palette.success.main,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledFailedIcon = styled(ErrorIcon)(({ theme }) => ({
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IProjectActionsLastEventProps {
|
||||||
|
action: IActionSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectActionsLastEvent = ({
|
||||||
|
action,
|
||||||
|
}: IProjectActionsLastEventProps) => {
|
||||||
|
const { id, project } = action;
|
||||||
|
const { actionEvents } = useActionEvents(id, project, 1, {
|
||||||
|
refreshInterval: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (actionEvents.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionSetEvent = actionEvents[0];
|
||||||
|
|
||||||
|
const icon =
|
||||||
|
actionSetEvent.state === 'success' ? (
|
||||||
|
<StyledSuccessIcon />
|
||||||
|
) : actionSetEvent.state === 'failed' ? (
|
||||||
|
<StyledFailedIcon />
|
||||||
|
) : (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledTooltipLink
|
||||||
|
tooltipProps={{
|
||||||
|
maxWidth: 500,
|
||||||
|
maxHeight: 600,
|
||||||
|
}}
|
||||||
|
tooltip={<ProjectActionsEventsDetails {...actionSetEvent} />}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</StyledTooltipLink>
|
||||||
|
);
|
||||||
|
};
|
@ -19,5 +19,11 @@ export const ProjectActionsActorCell = ({
|
|||||||
return <TextCell>No service account</TextCell>;
|
return <TextCell>No service account</TextCell>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <LinkCell to='/admin/service-accounts'>{actor.name}</LinkCell>;
|
return (
|
||||||
|
<LinkCell
|
||||||
|
to='/admin/service-accounts'
|
||||||
|
title={actor.name}
|
||||||
|
subtitle={actor.username}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,10 @@ const StyledDetails = styled('div')(({ theme }) => ({
|
|||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledAlert = styled(Alert)({
|
||||||
|
fontSize: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
export const ProjectActionsEventsDetails = ({
|
export const ProjectActionsEventsDetails = ({
|
||||||
state,
|
state,
|
||||||
actionSet: { actions },
|
actionSet: { actions },
|
||||||
@ -24,9 +28,9 @@ export const ProjectActionsEventsDetails = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDetails>
|
<StyledDetails>
|
||||||
<Alert severity={state === 'failed' ? 'error' : 'success'}>
|
<StyledAlert severity={state === 'failed' ? 'error' : 'success'}>
|
||||||
{stateText}
|
{stateText}
|
||||||
</Alert>
|
</StyledAlert>
|
||||||
<ProjectActionsEventsDetailsSource signal={signal} />
|
<ProjectActionsEventsDetailsSource signal={signal} />
|
||||||
{actions.map((action, i) => (
|
{actions.map((action, i) => (
|
||||||
<ProjectActionsEventsDetailsAction
|
<ProjectActionsEventsDetailsAction
|
||||||
|
@ -44,6 +44,7 @@ export const StyledFailedIcon = styled(ErrorOutline)(({ theme }) => ({
|
|||||||
|
|
||||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
|
fontSize: 'inherit',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledDivider = styled(Divider)(({ theme }) => ({
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
|
@ -24,6 +24,10 @@ const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledAccordionSummary = styled(AccordionSummary)({
|
||||||
|
lineHeight: '1.5rem',
|
||||||
|
});
|
||||||
|
|
||||||
const StyledLink = styled(Link)(({ theme }) => ({
|
const StyledLink = styled(Link)(({ theme }) => ({
|
||||||
marginLeft: theme.spacing(1),
|
marginLeft: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
@ -47,7 +51,7 @@ export const ProjectActionsEventsDetailsSourceSignalEndpoint = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAccordion>
|
<StyledAccordion>
|
||||||
<AccordionSummary
|
<StyledAccordionSummary
|
||||||
expandIcon={
|
expandIcon={
|
||||||
<IconButton>
|
<IconButton>
|
||||||
<ExpandMore titleAccess='Toggle' />
|
<ExpandMore titleAccess='Toggle' />
|
||||||
@ -58,7 +62,7 @@ export const ProjectActionsEventsDetailsSourceSignalEndpoint = ({
|
|||||||
<StyledLink to='/integrations/signals'>
|
<StyledLink to='/integrations/signals'>
|
||||||
{signalEndpointName}
|
{signalEndpointName}
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</AccordionSummary>
|
</StyledAccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<LazyReactJSONEditor
|
<LazyReactJSONEditor
|
||||||
|
@ -2,6 +2,7 @@ import { styled, Typography } from '@mui/material';
|
|||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
import { IActionSet } from 'interfaces/action';
|
import { IActionSet } from 'interfaces/action';
|
||||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||||
|
import { formatOperatorDescription } from 'component/common/NewConstraintAccordion/ConstraintOperator/formatOperatorDescription';
|
||||||
|
|
||||||
const StyledItem = styled(Typography)(({ theme }) => ({
|
const StyledItem = styled(Typography)(({ theme }) => ({
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
@ -24,6 +25,9 @@ export const ProjectActionsFiltersCell = ({
|
|||||||
return (
|
return (
|
||||||
<TextCell>
|
<TextCell>
|
||||||
<TooltipLink
|
<TooltipLink
|
||||||
|
tooltipProps={{
|
||||||
|
maxWidth: 500,
|
||||||
|
}}
|
||||||
tooltip={
|
tooltip={
|
||||||
<>
|
<>
|
||||||
{filters.map(
|
{filters.map(
|
||||||
@ -36,18 +40,33 @@ export const ProjectActionsFiltersCell = ({
|
|||||||
value,
|
value,
|
||||||
values,
|
values,
|
||||||
},
|
},
|
||||||
]) => (
|
]) => {
|
||||||
<StyledItem key={parameter}>
|
const operatorDescription: string =
|
||||||
<strong>{parameter}</strong>{' '}
|
formatOperatorDescription(operator);
|
||||||
{inverted ? 'NOT' : ''} {operator}{' '}
|
|
||||||
{caseInsensitive
|
const operatorText = inverted ? (
|
||||||
? '(case insensitive)'
|
<>
|
||||||
: ''}{' '}
|
is <u>not</u>{' '}
|
||||||
<strong>
|
{operatorDescription.substring(2)}
|
||||||
{values ? values.join(', ') : value}
|
</>
|
||||||
</strong>
|
) : (
|
||||||
</StyledItem>
|
operatorDescription
|
||||||
),
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledItem key={parameter}>
|
||||||
|
<strong>{parameter}</strong>{' '}
|
||||||
|
{operatorText}
|
||||||
|
{caseInsensitive
|
||||||
|
? ' (case insensitive)'
|
||||||
|
: ''}
|
||||||
|
{': '}
|
||||||
|
<strong>
|
||||||
|
{values ? values.join(', ') : value}
|
||||||
|
</strong>
|
||||||
|
</StyledItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ export const useProjectActionsForm = (action?: IActionSet) => {
|
|||||||
|
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
const [sourceId, setSourceId] = useState<number>(0);
|
const [sourceId, setSourceId] = useState<number>(0);
|
||||||
const [filters, setFilters] = useState<ActionsFilterState[]>([]);
|
const [filters, setFilters] = useState<ActionsFilterState[]>([]);
|
||||||
const [actorId, setActorId] = useState<number>(0);
|
const [actorId, setActorId] = useState<number>(0);
|
||||||
@ -49,6 +50,7 @@ export const useProjectActionsForm = (action?: IActionSet) => {
|
|||||||
const reloadForm = () => {
|
const reloadForm = () => {
|
||||||
setEnabled(action?.enabled ?? true);
|
setEnabled(action?.enabled ?? true);
|
||||||
setName(action?.name || '');
|
setName(action?.name || '');
|
||||||
|
setDescription(action?.description || '');
|
||||||
setSourceId(action?.match?.sourceId ?? 0);
|
setSourceId(action?.match?.sourceId ?? 0);
|
||||||
setFilters(
|
setFilters(
|
||||||
Object.entries(action?.match?.payload ?? {}).map(
|
Object.entries(action?.match?.payload ?? {}).map(
|
||||||
@ -171,6 +173,8 @@ export const useProjectActionsForm = (action?: IActionSet) => {
|
|||||||
setEnabled,
|
setEnabled,
|
||||||
name,
|
name,
|
||||||
setName,
|
setName,
|
||||||
|
description,
|
||||||
|
setDescription,
|
||||||
sourceId,
|
sourceId,
|
||||||
setSourceId,
|
setSourceId,
|
||||||
filters,
|
filters,
|
||||||
|
@ -69,6 +69,8 @@ export const ProjectActionsModal = ({
|
|||||||
setEnabled,
|
setEnabled,
|
||||||
name,
|
name,
|
||||||
setName,
|
setName,
|
||||||
|
description,
|
||||||
|
setDescription,
|
||||||
sourceId,
|
sourceId,
|
||||||
setSourceId,
|
setSourceId,
|
||||||
filters,
|
filters,
|
||||||
@ -94,6 +96,7 @@ export const ProjectActionsModal = ({
|
|||||||
const payload: ActionSetPayload = {
|
const payload: ActionSetPayload = {
|
||||||
enabled,
|
enabled,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
match: {
|
match: {
|
||||||
source: 'signal-endpoint',
|
source: 'signal-endpoint',
|
||||||
sourceId,
|
sourceId,
|
||||||
|
@ -31,43 +31,43 @@ const StyledLink = styled(Link)<{
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface IProjectActionsTriggerCellProps {
|
interface IProjectActionsSourceCellProps {
|
||||||
action: IActionSet;
|
action: IActionSet;
|
||||||
signalEndpoints: ISignalEndpoint[];
|
signalEndpoints: ISignalEndpoint[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectActionsTriggerCell = ({
|
export const ProjectActionsSourceCell = ({
|
||||||
action,
|
action,
|
||||||
signalEndpoints,
|
signalEndpoints,
|
||||||
}: IProjectActionsTriggerCellProps) => {
|
}: IProjectActionsSourceCellProps) => {
|
||||||
const { sourceId, source } = action.match;
|
const { sourceId, source } = action.match;
|
||||||
const trigger = signalEndpoints.find(({ id }) => id === sourceId);
|
|
||||||
|
|
||||||
if (!trigger) {
|
if (source === 'signal-endpoint') {
|
||||||
return <TextCell>No trigger</TextCell>;
|
const signalEndpoint = signalEndpoints.find(
|
||||||
|
({ id }) => id === sourceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (signalEndpoint) {
|
||||||
|
return (
|
||||||
|
<TextCell>
|
||||||
|
<StyledCell>
|
||||||
|
<HtmlTooltip title='Signal endpoint' arrow>
|
||||||
|
<StyledIcon alt='Signal endpoint' variant='rounded'>
|
||||||
|
<SignalsIcon />
|
||||||
|
</StyledIcon>
|
||||||
|
</HtmlTooltip>
|
||||||
|
<StyledLink
|
||||||
|
component={RouterLink}
|
||||||
|
to='/integrations/signals'
|
||||||
|
underline='hover'
|
||||||
|
>
|
||||||
|
{signalEndpoint.name}
|
||||||
|
</StyledLink>
|
||||||
|
</StyledCell>
|
||||||
|
</TextCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceIcon =
|
return <TextCell>No source</TextCell>;
|
||||||
source === 'signal-endpoint' ? (
|
|
||||||
<HtmlTooltip title='Signal endpoint' arrow>
|
|
||||||
<StyledIcon alt='Signal endpoint' variant='rounded'>
|
|
||||||
<SignalsIcon />
|
|
||||||
</StyledIcon>
|
|
||||||
</HtmlTooltip>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextCell>
|
|
||||||
<StyledCell>
|
|
||||||
{sourceIcon}
|
|
||||||
<StyledLink
|
|
||||||
component={RouterLink}
|
|
||||||
to='/integrations/signals'
|
|
||||||
underline='hover'
|
|
||||||
>
|
|
||||||
{trigger.name}
|
|
||||||
</StyledLink>
|
|
||||||
</StyledCell>
|
|
||||||
</TextCell>
|
|
||||||
);
|
|
||||||
};
|
};
|
@ -13,10 +13,10 @@ import { useActions } from 'hooks/api/getters/useActions/useActions';
|
|||||||
import { useActionsApi } from 'hooks/api/actions/useActionsApi/useActionsApi';
|
import { useActionsApi } from 'hooks/api/actions/useActionsApi/useActionsApi';
|
||||||
import { IActionSet } from 'interfaces/action';
|
import { IActionSet } from 'interfaces/action';
|
||||||
import { ToggleCell } from 'component/common/Table/cells/ToggleCell/ToggleCell';
|
import { ToggleCell } from 'component/common/Table/cells/ToggleCell/ToggleCell';
|
||||||
import { ProjectActionsTriggerCell } from './ProjectActionsTriggerCell';
|
import { ProjectActionsSourceCell } from './ProjectActionsSourceCell';
|
||||||
import { ProjectActionsFiltersCell } from './ProjectActionsFiltersCell';
|
import { ProjectActionsFiltersCell } from './ProjectActionsFiltersCell';
|
||||||
import { ProjectActionsActorCell } from './ProjectActionsActorCell';
|
import { ProjectActionsActorCell } from './ProjectActionsActorCell';
|
||||||
import { ProjectActionsActionsCell } from './ProjectActionsActionsCell';
|
import { ProjectActionsActionsCell } from './ProjectActionsActionsCell/ProjectActionsActionsCell';
|
||||||
import { ProjectActionsTableActionsCell } from './ProjectActionsTableActionsCell';
|
import { ProjectActionsTableActionsCell } from './ProjectActionsTableActionsCell';
|
||||||
import { ProjectActionsModal } from './ProjectActionsModal/ProjectActionsModal';
|
import { ProjectActionsModal } from './ProjectActionsModal/ProjectActionsModal';
|
||||||
import { ProjectActionsDeleteDialog } from './ProjectActionsDeleteDialog';
|
import { ProjectActionsDeleteDialog } from './ProjectActionsDeleteDialog';
|
||||||
@ -96,6 +96,7 @@ export const ProjectActionsTable = ({
|
|||||||
}: { row: { original: IActionSet } }) => (
|
}: { row: { original: IActionSet } }) => (
|
||||||
<LinkCell
|
<LinkCell
|
||||||
title={action.name}
|
title={action.name}
|
||||||
|
subtitle={action.description}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedAction(action);
|
setSelectedAction(action);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
@ -104,12 +105,12 @@ export const ProjectActionsTable = ({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'trigger',
|
id: 'source',
|
||||||
Header: 'Trigger',
|
Header: 'Source',
|
||||||
Cell: ({
|
Cell: ({
|
||||||
row: { original: action },
|
row: { original: action },
|
||||||
}: { row: { original: IActionSet } }) => (
|
}: { row: { original: IActionSet } }) => (
|
||||||
<ProjectActionsTriggerCell
|
<ProjectActionsSourceCell
|
||||||
action={action}
|
action={action}
|
||||||
signalEndpoints={signalEndpoints}
|
signalEndpoints={signalEndpoints}
|
||||||
/>
|
/>
|
||||||
|
@ -9,6 +9,7 @@ export interface IActionSet {
|
|||||||
id: number;
|
id: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
description: string;
|
||||||
project: string;
|
project: string;
|
||||||
actorId: number;
|
actorId: number;
|
||||||
match: IMatch;
|
match: IMatch;
|
||||||
|
Loading…
Reference in New Issue
Block a user