diff --git a/frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCard.tsx b/frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCard.tsx index fe564e0c97..19f8b2ed26 100644 --- a/frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCard.tsx +++ b/frontend/src/component/integrations/IntegrationList/IntegrationCard/IntegrationCard.tsx @@ -59,10 +59,10 @@ const StyledCard = styled('div', { '&:after': { content: '""', width: 'auto', - height: theme.spacing(0.75), + height: theme.spacing(1), position: 'absolute', zIndex: -1, - bottom: theme.spacing(-0.75), + bottom: theme.spacing(-1), left: theme.spacing(1), right: theme.spacing(1), borderBottomLeftRadius: `${theme.shape.borderRadiusMedium}px`, diff --git a/frontend/src/component/integrations/IntegrationList/IntegrationIcon/IntegrationIcon.tsx b/frontend/src/component/integrations/IntegrationList/IntegrationIcon/IntegrationIcon.tsx index 3d0e68209f..da5941af5e 100644 --- a/frontend/src/component/integrations/IntegrationList/IntegrationIcon/IntegrationIcon.tsx +++ b/frontend/src/component/integrations/IntegrationList/IntegrationIcon/IntegrationIcon.tsx @@ -32,6 +32,7 @@ interface IIntegrationIconProps { } const StyledAvatar = styled(Avatar)(({ theme }) => ({ + background: 'transparent', marginRight: theme.spacing(2), borderRadius: theme.shape.borderRadius, overflow: 'hidden', @@ -52,7 +53,7 @@ const StyledCustomIcon = styled(Icon)({ }); const StyledSignalsIcon = styled(StyledCustomIcon)(({ theme }) => ({ - background: theme.palette.primary.main, + background: theme.palette.background.alternative, color: theme.palette.primary.contrastText, })); diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsActionsCell/ProjectActionsActionsCell.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsActionsCell/ProjectActionsActionsCell.tsx index 29b13bd0bc..7262bd41bb 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsActionsCell/ProjectActionsActionsCell.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsActionsCell/ProjectActionsActionsCell.tsx @@ -9,6 +9,7 @@ const StyledCell = styled('div')(({ theme }) => ({ display: 'flex', alignItems: 'center', gap: theme.spacing(1), + padding: theme.spacing(2), })); const StyledActionItems = styled('div')(({ theme }) => ({ diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsActionsCell/ProjectActionsLastEvent.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsActionsCell/ProjectActionsLastEvent.tsx index 7e0ad18a8f..5920e1bd14 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsActionsCell/ProjectActionsLastEvent.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsActionsCell/ProjectActionsLastEvent.tsx @@ -3,18 +3,32 @@ 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'; +import { CheckCircleOutline, ErrorOutline } from '@mui/icons-material'; +import { useLocationSettings } from 'hooks/useLocationSettings'; +import { formatDateYMDHMS } from 'utils/formatDate'; const StyledTooltipLink = styled(TooltipLink)(({ theme }) => ({ display: 'flex', alignItems: 'center', })); -export const StyledSuccessIcon = styled(CheckCircle)(({ theme }) => ({ +const StyledTitle = styled('div')(({ theme }) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + margin: theme.spacing(0, 2), + marginTop: theme.spacing(2), +})); + +const StyledLastEventSpan = styled('span')(({ theme }) => ({ + fontSize: theme.fontSizes.bodySize, +})); + +export const StyledSuccessIcon = styled(CheckCircleOutline)(({ theme }) => ({ color: theme.palette.success.main, })); -export const StyledFailedIcon = styled(ErrorIcon)(({ theme }) => ({ +export const StyledFailedIcon = styled(ErrorOutline)(({ theme }) => ({ color: theme.palette.error.main, })); @@ -29,6 +43,7 @@ export const ProjectActionsLastEvent = ({ const { actionEvents } = useActionEvents(id, project, 1, { refreshInterval: 5000, }); + const { locationSettings } = useLocationSettings(); if (actionEvents.length === 0) { return null; @@ -51,7 +66,20 @@ export const ProjectActionsLastEvent = ({ maxWidth: 500, maxHeight: 600, }} - tooltip={} + tooltip={ + <> + + Last event + + {formatDateYMDHMS( + actionSetEvent.createdAt, + locationSettings?.locale, + )} + + + + + } > {icon} diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails/ProjectActionsEventsDetails.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails/ProjectActionsEventsDetails.tsx index 0bca4b1651..d08d8e3cce 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails/ProjectActionsEventsDetails.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails/ProjectActionsEventsDetails.tsx @@ -2,6 +2,7 @@ import { Alert, styled } from '@mui/material'; import { IActionSetEvent } from 'interfaces/action'; import { ProjectActionsEventsDetailsAction } from './ProjectActionsEventsDetailsAction'; import { ProjectActionsEventsDetailsSource } from './ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSource'; +import { CheckCircleOutline } from '@mui/icons-material'; const StyledDetails = styled('div')(({ theme }) => ({ display: 'flex', @@ -28,7 +29,10 @@ export const ProjectActionsEventsDetails = ({ return ( - + : undefined} + > {stateText} diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails/ProjectActionsEventsDetailsAction.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails/ProjectActionsEventsDetailsAction.tsx index 4d23f28b26..24e9becfff 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails/ProjectActionsEventsDetailsAction.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails/ProjectActionsEventsDetailsAction.tsx @@ -76,13 +76,11 @@ export const ProjectActionsEventsDetailsAction = ({ const actionState = state === 'success' ? ( - ) : state === 'failed' ? ( - ) : state === 'started' ? ( - ) : ( + ) : state === 'not started' ? ( Not started - ); + ) : null; return ( diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsStateCell.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsStateCell.tsx index 74006a84fd..a6635cfa48 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsStateCell.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsStateCell.tsx @@ -1,12 +1,12 @@ import { CircularProgress, styled } from '@mui/material'; -import { CheckCircle, Error as ErrorIcon } from '@mui/icons-material'; +import { CheckCircleOutline, ErrorOutline } from '@mui/icons-material'; import { IActionSetEvent } from 'interfaces/action'; -export const StyledSuccessIcon = styled(CheckCircle)(({ theme }) => ({ +export const StyledSuccessIcon = styled(CheckCircleOutline)(({ theme }) => ({ color: theme.palette.success.main, })); -export const StyledFailedIcon = styled(ErrorIcon)(({ theme }) => ({ +export const StyledFailedIcon = styled(ErrorOutline)(({ theme }) => ({ color: theme.palette.error.main, })); diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsActionItem.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsActionItem.tsx deleted file mode 100644 index 94bcd89ac7..0000000000 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsActionItem.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { IconButton, Tooltip, styled } from '@mui/material'; -import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; -import { Delete } from '@mui/icons-material'; -import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; -import { ActionsActionState } from './useProjectActionsForm'; -import { ProjectActionsFormItem } from './ProjectActionsFormItem'; -import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; - -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, - index, - stateChanged, - onDelete, -}: { - action: ActionsActionState; - index: number; - stateChanged: (action: ActionsActionState) => void; - onDelete: () => void; -}) => { - const { action: actionName } = action; - const projectId = useRequiredPathParam('projectId'); - const { project } = useProjectOverview(projectId); - - const environments = project.environments.map( - ({ environment }) => environment, - ); - - const { features } = useFeatureSearch({ project: `IS:${projectId}` }); - - const header = ( - <> - Action {index + 1} -
- - - - - -
- - ); - - return ( - - - - - stateChanged({ - ...action, - action: selected, - }) - } - fullWidth - /> - - - ({ - label: environment, - key: environment, - }))} - value={action.executionParams.environment as string} - onChange={(selected) => - stateChanged({ - ...action, - executionParams: { - ...action.executionParams, - environment: selected, - }, - }) - } - fullWidth - /> - - - ({ - label: feature.name, - key: feature.name, - }))} - value={action.executionParams.featureName as string} - onChange={(selected) => - stateChanged({ - ...action, - executionParams: { - ...action.executionParams, - featureName: selected, - }, - }) - } - fullWidth - /> - - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsForm.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsForm.tsx index 92b93ec694..fe173ab102 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsForm.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsForm.tsx @@ -1,4 +1,4 @@ -import { Alert, Button, Divider, Link, styled } from '@mui/material'; +import { Alert, Link, styled } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; import Input from 'component/common/Input/Input'; import { FormSwitch } from 'component/common/FormSwitch/FormSwitch'; @@ -9,17 +9,8 @@ import { ProjectActionsFormErrors, } from './useProjectActionsForm'; import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts'; -import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints'; -import { v4 as uuidv4 } from 'uuid'; -import { useMemo } from 'react'; -import GeneralSelect, {} from 'component/common/GeneralSelect/GeneralSelect'; -import { Add } from '@mui/icons-material'; -import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import { ProjectActionsActionItem } from './ProjectActionsActionItem'; -import { ProjectActionsFilterItem } from './ProjectActionsFilterItem'; -import { ProjectActionsFormStep } from './ProjectActionsFormStep'; -import { IN } from 'constants/operators'; -import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; +import { ProjectActionsFormStepSource } from './ProjectActionsFormStep/ProjectActionsFormStepSource/ProjectActionsFormStepSource'; +import { ProjectActionsFormStepActions } from './ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsFormStepActions'; const StyledServiceAccountAlert = styled(Alert)(({ theme }) => ({ marginBottom: theme.spacing(4), @@ -45,21 +36,8 @@ const StyledInput = styled(Input)(() => ({ width: '100%', })); -const StyledButtonContainer = styled('div')(({ theme }) => ({ - display: 'flex', - marginTop: theme.spacing(1), - gap: theme.spacing(1), -})); - -const StyledDivider = styled(Divider)(({ theme }) => ({ - margin: theme.spacing(3, 0), - marginBottom: theme.spacing(2), -})); - -const StyledTooltip = styled('div')(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(1), +const StyledAlert = styled(Alert)(({ theme }) => ({ + marginTop: theme.spacing(3), })); interface IProjectActionsFormProps { @@ -67,6 +45,8 @@ interface IProjectActionsFormProps { setEnabled: React.Dispatch>; name: string; setName: React.Dispatch>; + description: string; + setDescription: React.Dispatch>; sourceId: number; setSourceId: React.Dispatch>; filters: ActionsFilterState[]; @@ -85,6 +65,8 @@ export const ProjectActionsForm = ({ setEnabled, name, setName, + description, + setDescription, sourceId, setSourceId, filters, @@ -97,82 +79,13 @@ export const ProjectActionsForm = ({ validateName, validated, }: IProjectActionsFormProps) => { - const projectId = useRequiredPathParam('projectId'); const { serviceAccounts, loading: serviceAccountsLoading } = useServiceAccounts(); - const { signalEndpoints, loading: signalEndpointsLoading } = - useSignalEndpoints(); const handleOnBlur = (callback: Function) => { setTimeout(() => callback(), 300); }; - const addFilter = () => { - const id = uuidv4(); - setFilters((filters) => [ - ...filters, - { - id, - parameter: '', - operator: IN, - }, - ]); - }; - - const updateInFilters = (updatedFilter: ActionsFilterState) => { - setFilters((filters) => - filters.map((filter) => - filter.id === updatedFilter.id ? updatedFilter : filter, - ), - ); - }; - - const addAction = (projectId: string) => { - const id = uuidv4(); - const action: ActionsActionState = { - id, - action: '', - sortOrder: - actions - .map((a) => a.sortOrder) - .reduce((a, b) => Math.max(a, b), 0) + 1, - executionParams: { - project: projectId, - }, - }; - setActions([...actions, action]); - }; - - const updateInActions = (updatedAction: ActionsActionState) => { - setActions((actions) => - actions.map((action) => - action.id === updatedAction.id ? updatedAction : action, - ), - ); - }; - - const signalEndpointOptions = useMemo(() => { - if (signalEndpointsLoading) { - return []; - } - - return signalEndpoints.map(({ id, name }) => ({ - label: name, - key: `${id}`, - })); - }, [signalEndpointsLoading, signalEndpoints]); - - const serviceAccountOptions = useMemo(() => { - if (serviceAccountsLoading) { - return []; - } - - return serviceAccounts.map((sa) => ({ - label: sa.name, - key: `${sa.id}`, - })); - }, [serviceAccountsLoading, serviceAccounts]); - const showErrors = validated && Object.values(errors).some(Boolean); return ( @@ -214,116 +127,36 @@ export const ProjectActionsForm = ({ onBlur={(e) => handleOnBlur(() => validateName(e.target.value))} autoComplete='off' /> + + What is your new action description? + + setDescription(e.target.value)} + autoComplete='off' + /> - - Create signal endpoint - - } - > - { - setSourceId(parseInt(v)); - }} - /> - + - - {filters.map((filter, index) => ( - - setFilters((filters) => - filters.filter((f) => f.id !== filter.id), - ) - } - /> - ))} - - - -

- Filters allow you to add conditions to the - execution of the actions based on the source - payload. -

-

- If no filters are defined then the action - will always be triggered from the selected - source, no matter the payload. -

- - } - /> -
-
- - - Create service account - - } - > - { - setActorId(parseInt(v)); - }} - /> - - {actions.map((action, index) => ( - - setActions((actions) => - actions.filter((a) => a.id !== action.id), - ) - } - /> - ))} - - - - + ( - +
    {Object.values(errors) .filter(Boolean) @@ -331,7 +164,7 @@ export const ProjectActionsForm = ({
  • {error}
  • ))}
-
+
)} /> diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormItem.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormItem.tsx similarity index 100% rename from frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormItem.tsx rename to frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormItem.tsx diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStep.tsx similarity index 100% rename from frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep.tsx rename to frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStep.tsx diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionItem.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionItem.tsx new file mode 100644 index 0000000000..5949dd2dba --- /dev/null +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionItem.tsx @@ -0,0 +1,185 @@ +import { Alert, IconButton, Tooltip, styled } from '@mui/material'; +import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; +import { Delete } from '@mui/icons-material'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; +import { ActionsActionState } from '../../useProjectActionsForm'; +import { ProjectActionsFormItem } from '../ProjectActionsFormItem'; +import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useServiceAccountAccessMatrix } from 'hooks/api/getters/useServiceAccountAccessMatrix/useServiceAccountAccessMatrix'; +import { useMemo } from 'react'; + +const StyledItemBody = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + width: '100%', + gap: theme.spacing(2), +})); + +const StyledItemRow = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + width: '100%', +})); + +const StyledFieldContainer = styled('div')({ + flex: 1, +}); + +const options = [ + { + label: 'Enable flag', + key: 'TOGGLE_FEATURE_ON', + permissions: ['UPDATE_FEATURE_ENVIRONMENT'], + }, + { + label: 'Disable flag', + key: 'TOGGLE_FEATURE_OFF', + permissions: ['UPDATE_FEATURE_ENVIRONMENT'], + }, +]; + +interface IProjectActionsItemProps { + action: ActionsActionState; + index: number; + stateChanged: (action: ActionsActionState) => void; + actorId: number; + onDelete: () => void; +} + +export const ProjectActionsActionItem = ({ + action, + index, + stateChanged, + actorId, + onDelete, +}: IProjectActionsItemProps) => { + const { action: actionName } = action; + const projectId = useRequiredPathParam('projectId'); + const { project } = useProjectOverview(projectId); + const { permissions } = useServiceAccountAccessMatrix( + actorId, + projectId, + action.executionParams.environment as string, + ); + + const hasPermission = useMemo(() => { + const requiredPermissions = options.find( + ({ key }) => key === actionName, + )?.permissions; + + const { environment: actionEnvironment } = action.executionParams; + + if ( + permissions.length === 0 || + !requiredPermissions || + !actionEnvironment + ) { + return true; + } + + return requiredPermissions.some((requiredPermission) => + permissions.some( + ({ permission, environment }) => + permission === requiredPermission && + environment === actionEnvironment, + ), + ); + }, [actionName, permissions]); + + const environments = project.environments.map( + ({ environment }) => environment, + ); + + const { features } = useFeatureSearch({ project: `IS:${projectId}` }); + + const header = ( + <> + Action {index + 1} +
+ + + + + +
+ + ); + + return ( + + + + + + stateChanged({ + ...action, + action: selected, + }) + } + fullWidth + /> + + + ({ + label: environment, + key: environment, + }))} + value={action.executionParams.environment as string} + onChange={(selected) => + stateChanged({ + ...action, + executionParams: { + ...action.executionParams, + environment: selected, + }, + }) + } + fullWidth + /> + + + ({ + label: feature.name, + key: feature.name, + }))} + value={action.executionParams.featureName as string} + onChange={(selected) => + stateChanged({ + ...action, + executionParams: { + ...action.executionParams, + featureName: selected, + }, + }) + } + fullWidth + /> + + + + The selected service account does not have + permissions to execute this action currently. + + } + /> + + + ); +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsFormStepActions.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsFormStepActions.tsx new file mode 100644 index 0000000000..a31263442a --- /dev/null +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsFormStepActions.tsx @@ -0,0 +1,124 @@ +import { useMemo } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { Button, Divider, styled } from '@mui/material'; +import { v4 as uuidv4 } from 'uuid'; +import { ProjectActionsActionItem } from './ProjectActionsActionItem'; +import { ActionsActionState } from '../../useProjectActionsForm'; +import { ProjectActionsFormStep } from '../ProjectActionsFormStep'; +import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; +import { Add } from '@mui/icons-material'; +import { IServiceAccount } from 'interfaces/service-account'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; + +const StyledDivider = styled(Divider)(({ theme }) => ({ + margin: theme.spacing(2, 0), + marginBottom: theme.spacing(1), + borderStyle: 'dashed', +})); + +const StyledButtonContainer = styled('div')(({ theme }) => ({ + display: 'flex', + marginTop: theme.spacing(1), + gap: theme.spacing(1), +})); + +interface IProjectActionsFormStepActionsProps { + serviceAccounts: IServiceAccount[]; + serviceAccountsLoading: boolean; + actions: ActionsActionState[]; + setActions: React.Dispatch>; + actorId: number; + setActorId: React.Dispatch>; +} + +export const ProjectActionsFormStepActions = ({ + serviceAccounts, + serviceAccountsLoading, + actions, + setActions, + actorId, + setActorId, +}: IProjectActionsFormStepActionsProps) => { + const projectId = useRequiredPathParam('projectId'); + + const addAction = (projectId: string) => { + const id = uuidv4(); + const action: ActionsActionState = { + id, + action: '', + sortOrder: + actions + .map((a) => a.sortOrder) + .reduce((a, b) => Math.max(a, b), 0) + 1, + executionParams: { + project: projectId, + }, + }; + setActions([...actions, action]); + }; + + const updateInActions = (updatedAction: ActionsActionState) => { + setActions((actions) => + actions.map((action) => + action.id === updatedAction.id ? updatedAction : action, + ), + ); + }; + + const serviceAccountOptions = useMemo(() => { + if (serviceAccountsLoading) { + return []; + } + + return serviceAccounts.map((sa) => ({ + label: sa.name, + key: `${sa.id}`, + })); + }, [serviceAccountsLoading, serviceAccounts]); + return ( + + Create service account + + } + > + { + setActorId(parseInt(v)); + }} + /> + + {actions.map((action, index) => ( + + setActions((actions) => + actions.filter((a) => a.id !== action.id), + ) + } + /> + ))} + + + + + ); +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFilterItem.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepSource/ProjectActionsFilterItem.tsx similarity index 91% rename from frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFilterItem.tsx rename to frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepSource/ProjectActionsFilterItem.tsx index cc96a69490..b9a5fb8794 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFilterItem.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepSource/ProjectActionsFilterItem.tsx @@ -1,8 +1,14 @@ -import { IconButton, Tooltip, styled } from '@mui/material'; -import { ActionsFilterState } from './useProjectActionsForm'; +import { + Autocomplete, + IconButton, + TextField, + Tooltip, + styled, +} from '@mui/material'; +import { ActionsFilterState } from '../../useProjectActionsForm'; import { Delete } from '@mui/icons-material'; import Input from 'component/common/Input/Input'; -import { ProjectActionsFormItem } from './ProjectActionsFormItem'; +import { ProjectActionsFormItem } from '../ProjectActionsFormItem'; import { ConstraintOperatorSelect } from 'component/common/ConstraintAccordion/ConstraintOperatorSelect'; import { Operator, @@ -99,6 +105,7 @@ interface IProjectActionsFilterItemProps { filter: ActionsFilterState; index: number; stateChanged: (updatedFilter: ActionsFilterState) => void; + suggestions: string[]; onDelete: () => void; } @@ -106,6 +113,7 @@ export const ProjectActionsFilterItem = ({ filter, index, stateChanged, + suggestions, onDelete, }: IProjectActionsFilterItemProps) => { const { parameter, inverted, operator, caseInsensitive, value, values } = @@ -211,15 +219,23 @@ export const ProjectActionsFilterItem = ({ - + onInputChange={(_, parameter) => stateChanged({ ...filter, - parameter: e.target.value, + parameter, }) } + renderInput={(params) => ( + + )} /> diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepSource/ProjectActionsFormStepSource.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepSource/ProjectActionsFormStepSource.tsx new file mode 100644 index 0000000000..90e3875efe --- /dev/null +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepSource/ProjectActionsFormStepSource.tsx @@ -0,0 +1,158 @@ +import { useMemo } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { Button, Divider, styled } from '@mui/material'; +import { v4 as uuidv4 } from 'uuid'; +import { IN } from 'constants/operators'; +import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints'; +import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; +import { ProjectActionsFilterItem } from './ProjectActionsFilterItem'; +import { ActionsFilterState } from '../../useProjectActionsForm'; +import { ProjectActionsFormStep } from '../ProjectActionsFormStep'; +import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; +import { Add } from '@mui/icons-material'; +import { ProjectActionsPreviewPayload } from './ProjectActionsPreviewPayload'; +import { useSignalEndpointSignals } from 'hooks/api/getters/useSignalEndpointSignals/useSignalEndpointSignals'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; + +const StyledDivider = styled(Divider)(({ theme }) => ({ + margin: theme.spacing(2, 0), + marginBottom: theme.spacing(1), + borderStyle: 'dashed', +})); + +const StyledTooltip = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), +})); + +const StyledButtonContainer = styled('div')(({ theme }) => ({ + display: 'flex', + marginTop: theme.spacing(1), + gap: theme.spacing(1), +})); + +interface IProjectActionsFormStepSourceProps { + sourceId: number; + setSourceId: React.Dispatch>; + filters: ActionsFilterState[]; + setFilters: React.Dispatch>; +} + +export const ProjectActionsFormStepSource = ({ + sourceId, + setSourceId, + filters, + setFilters, +}: IProjectActionsFormStepSourceProps) => { + const { signalEndpoints, loading: signalEndpointsLoading } = + useSignalEndpoints(); + const { signalEndpointSignals } = useSignalEndpointSignals(sourceId, 1); + + const addFilter = () => { + const id = uuidv4(); + setFilters((filters) => [ + ...filters, + { + id, + parameter: '', + operator: IN, + }, + ]); + }; + + const updateInFilters = (updatedFilter: ActionsFilterState) => { + setFilters((filters) => + filters.map((filter) => + filter.id === updatedFilter.id ? updatedFilter : filter, + ), + ); + }; + + const signalEndpointOptions = useMemo(() => { + if (signalEndpointsLoading) { + return []; + } + + return signalEndpoints.map(({ id, name }) => ({ + label: name, + key: `${id}`, + })); + }, [signalEndpointsLoading, signalEndpoints]); + + const { lastSourcePayload, filterSuggestions } = useMemo(() => { + const lastSourcePayload = signalEndpointSignals[0]?.payload; + return { + lastSourcePayload, + filterSuggestions: Object.keys(lastSourcePayload || {}), + }; + }, [signalEndpointSignals]); + + return ( + + Create signal endpoint + + } + > + { + setSourceId(parseInt(v)); + }} + /> + + } + /> + + {filters.map((filter, index) => ( + + setFilters((filters) => + filters.filter(({ id }) => id !== filter.id), + ) + } + /> + ))} + + + +

+ Filters allow you to add conditions to the + execution of the actions based on the source + payload. +

+

+ If no filters are defined then the action will + always be triggered from the selected source, no + matter the payload. +

+ + } + /> +
+
+ ); +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepSource/ProjectActionsPreviewPayload.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepSource/ProjectActionsPreviewPayload.tsx new file mode 100644 index 0000000000..7306fac035 --- /dev/null +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepSource/ProjectActionsPreviewPayload.tsx @@ -0,0 +1,92 @@ +import { ArrowForwardIosSharp } from '@mui/icons-material'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + IconButton, + styled, +} from '@mui/material'; +import { Suspense, lazy } from 'react'; + +const LazyReactJSONEditor = lazy( + () => import('component/common/ReactJSONEditor/ReactJSONEditor'), +); + +const StyledNoSignalsSpan = styled('span')(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + marginTop: theme.spacing(1.5), + marginBottom: theme.spacing(-0.75), + height: theme.spacing(3), +})); + +const StyledAccordion = styled(Accordion)({ + backgroundColor: 'transparent', + boxShadow: 'none', + '&:before': { + display: 'none', + }, +}); + +const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({ + lineHeight: '1.5rem', + padding: 0, + marginBottom: theme.spacing(-2.25), + fontSize: theme.fontSizes.smallBody, + color: theme.palette.primary.main, + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(90deg)', + }, +})); + +const StyledArrowForwardIosSharp = styled(ArrowForwardIosSharp)( + ({ theme }) => ({ + color: theme.palette.primary.main, + fontSize: theme.fontSizes.smallBody, + }), +); + +const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({ + marginTop: theme.spacing(2), + padding: 0, +})); + +interface IProjectActionsPreviewPayloadProps { + payload?: unknown; +} + +export const ProjectActionsPreviewPayload = ({ + payload, +}: IProjectActionsPreviewPayloadProps) => { + if (!payload) { + return ( + + No signals were received from this source yet. + + ); + } + + return ( + + + + + } + > + Preview payload + + + + + + + + ); +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsModal.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsModal.tsx index 0222b37aa7..11ba571b96 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsModal.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsModal.tsx @@ -194,6 +194,8 @@ export const ProjectActionsModal = ({ setEnabled={setEnabled} name={name} setName={setName} + description={description} + setDescription={setDescription} sourceId={sourceId} setSourceId={setSourceId} filters={filters} diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsSourceCell.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsSourceCell.tsx index 87884c9f2e..4322acf0e3 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsSourceCell.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsSourceCell.tsx @@ -14,6 +14,7 @@ const StyledCell = styled(Box)({ }); const StyledIcon = styled(Avatar)(({ theme }) => ({ + background: 'transparent', borderRadius: theme.shape.borderRadius, overflow: 'hidden', width: theme.spacing(3), diff --git a/frontend/src/hooks/api/getters/useServiceAccountAccessMatrix/useServiceAccountAccessMatrix.ts b/frontend/src/hooks/api/getters/useServiceAccountAccessMatrix/useServiceAccountAccessMatrix.ts new file mode 100644 index 0000000000..ad05d8776a --- /dev/null +++ b/frontend/src/hooks/api/getters/useServiceAccountAccessMatrix/useServiceAccountAccessMatrix.ts @@ -0,0 +1,66 @@ +import { useMemo } from 'react'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import useSWR from 'swr'; +import { IRole } from 'interfaces/role'; +import { IServiceAccount } from 'interfaces/service-account'; +import { IMatrixPermission } from 'interfaces/permissions'; +import { IPermission } from 'interfaces/user'; + +interface IServiceAccountAccessMatrix { + root: IMatrixPermission[]; + project: IMatrixPermission[]; + environment: IMatrixPermission[]; +} + +interface IServiceAccountAccessMatrixResponse { + matrix: IServiceAccountAccessMatrix; + projectRoles: IRole[]; + rootRole: IRole; + serviceAccount: IServiceAccount; + permissions: IPermission[]; +} + +interface IServiceAccountAccessMatrixOutput + extends Partial { + permissions: IPermission[]; + loading: boolean; + refetch: () => void; + error?: Error; +} + +export const useServiceAccountAccessMatrix = ( + id: number, + project?: string, + environment?: string, +): IServiceAccountAccessMatrixOutput => { + const queryParams = `${project ? `?project=${project}` : ''}${ + environment ? `${project ? '&' : '?'}environment=${environment}` : '' + }`; + const url = `api/admin/service-account/${id}/permissions${queryParams}`; + + const { data, error, mutate } = useSWR( + formatApiPath(url), + fetcher, + ); + + return useMemo( + () => ({ + matrix: data?.matrix, + projectRoles: data?.projectRoles, + rootRole: data?.rootRole, + serviceAccount: data?.serviceAccount, + permissions: data?.permissions || [], + loading: !error && !data, + refetch: () => mutate(), + error, + }), + [data, error, mutate], + ); +}; + +const fetcher = (path: string) => { + return fetch(path) + .then(handleErrorResponses('Service account access matrix')) + .then((res) => res.json()); +};