mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
chore: new actions UI (#6448)
https://linear.app/unleash/issue/2-1995/ui-feature-rename-adapt-the-actions-ui Refreshes the UI for project actions according to the new designs and suggestions from @gastonfournier and @nicolaesocaciu Also includes some refactoring. ![image](https://github.com/Unleash/unleash/assets/14320932/83e8e8ed-46aa-471b-9d1d-0c051a298a9a)
This commit is contained in:
parent
85e9c934a9
commit
64593c57cf
@ -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`,
|
||||
|
@ -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,
|
||||
}));
|
||||
|
||||
|
@ -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 }) => ({
|
||||
|
@ -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={<ProjectActionsEventsDetails {...actionSetEvent} />}
|
||||
tooltip={
|
||||
<>
|
||||
<StyledTitle>
|
||||
<StyledLastEventSpan>Last event</StyledLastEventSpan>
|
||||
<span>
|
||||
{formatDateYMDHMS(
|
||||
actionSetEvent.createdAt,
|
||||
locationSettings?.locale,
|
||||
)}
|
||||
</span>
|
||||
</StyledTitle>
|
||||
<ProjectActionsEventsDetails {...actionSetEvent} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
</StyledTooltipLink>
|
||||
|
@ -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 (
|
||||
<StyledDetails>
|
||||
<StyledAlert severity={state === 'failed' ? 'error' : 'success'}>
|
||||
<StyledAlert
|
||||
severity={state === 'failed' ? 'error' : 'success'}
|
||||
icon={state === 'success' ? <CheckCircleOutline /> : undefined}
|
||||
>
|
||||
{stateText}
|
||||
</StyledAlert>
|
||||
<ProjectActionsEventsDetailsSource signal={signal} />
|
||||
|
@ -76,13 +76,11 @@ export const ProjectActionsEventsDetailsAction = ({
|
||||
const actionState =
|
||||
state === 'success' ? (
|
||||
<StyledSuccessIcon />
|
||||
) : state === 'failed' ? (
|
||||
<StyledFailedIcon />
|
||||
) : state === 'started' ? (
|
||||
<CircularProgress size={20} />
|
||||
) : (
|
||||
) : state === 'not started' ? (
|
||||
<span>Not started</span>
|
||||
);
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<StyledAction state={state}>
|
||||
|
@ -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,
|
||||
}));
|
||||
|
||||
|
@ -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 = (
|
||||
<>
|
||||
<span>Action {index + 1}</span>
|
||||
<div>
|
||||
<Tooltip title='Delete action' arrow>
|
||||
<IconButton onClick={onDelete}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<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.map((environment) => ({
|
||||
label: environment,
|
||||
key: environment,
|
||||
}))}
|
||||
value={action.executionParams.environment as string}
|
||||
onChange={(selected) =>
|
||||
stateChanged({
|
||||
...action,
|
||||
executionParams: {
|
||||
...action.executionParams,
|
||||
environment: selected,
|
||||
},
|
||||
})
|
||||
}
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledFieldContainer>
|
||||
</StyledItemRow>
|
||||
</ProjectActionsFormItem>
|
||||
);
|
||||
};
|
@ -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<React.SetStateAction<boolean>>;
|
||||
name: string;
|
||||
setName: React.Dispatch<React.SetStateAction<string>>;
|
||||
description: string;
|
||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||
sourceId: number;
|
||||
setSourceId: React.Dispatch<React.SetStateAction<number>>;
|
||||
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'
|
||||
/>
|
||||
<StyledInputDescription>
|
||||
What is your new action description?
|
||||
</StyledInputDescription>
|
||||
<StyledInput
|
||||
label='Action description'
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
autoComplete='off'
|
||||
/>
|
||||
|
||||
<ProjectActionsFormStep
|
||||
name='Trigger'
|
||||
resourceLink={
|
||||
<RouterLink to='/integrations/signals'>
|
||||
Create signal endpoint
|
||||
</RouterLink>
|
||||
}
|
||||
>
|
||||
<GeneralSelect
|
||||
label='Source'
|
||||
options={signalEndpointOptions}
|
||||
value={`${sourceId}`}
|
||||
onChange={(v) => {
|
||||
setSourceId(parseInt(v));
|
||||
}}
|
||||
/>
|
||||
</ProjectActionsFormStep>
|
||||
<ProjectActionsFormStepSource
|
||||
sourceId={sourceId}
|
||||
setSourceId={setSourceId}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
/>
|
||||
|
||||
<ProjectActionsFormStep name='When this' verticalConnector>
|
||||
{filters.map((filter, index) => (
|
||||
<ProjectActionsFilterItem
|
||||
key={filter.id}
|
||||
index={index}
|
||||
filter={filter}
|
||||
stateChanged={updateInFilters}
|
||||
onDelete={() =>
|
||||
setFilters((filters) =>
|
||||
filters.filter((f) => f.id !== filter.id),
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
startIcon={<Add />}
|
||||
onClick={addFilter}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
>
|
||||
Add filter
|
||||
</Button>
|
||||
<HelpIcon
|
||||
htmlTooltip
|
||||
tooltip={
|
||||
<StyledTooltip>
|
||||
<p>
|
||||
Filters allow you to add conditions to the
|
||||
execution of the actions based on the source
|
||||
payload.
|
||||
</p>
|
||||
<p>
|
||||
If no filters are defined then the action
|
||||
will always be triggered from the selected
|
||||
source, no matter the payload.
|
||||
</p>
|
||||
</StyledTooltip>
|
||||
}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</ProjectActionsFormStep>
|
||||
|
||||
<ProjectActionsFormStep
|
||||
name='Do these actions'
|
||||
verticalConnector
|
||||
resourceLink={
|
||||
<RouterLink to='/admin/service-accounts'>
|
||||
Create service account
|
||||
</RouterLink>
|
||||
}
|
||||
>
|
||||
<GeneralSelect
|
||||
label='Service account'
|
||||
name='service-account'
|
||||
options={serviceAccountOptions}
|
||||
value={`${actorId}`}
|
||||
onChange={(v) => {
|
||||
setActorId(parseInt(v));
|
||||
}}
|
||||
/>
|
||||
<StyledDivider />
|
||||
{actions.map((action, index) => (
|
||||
<ProjectActionsActionItem
|
||||
index={index}
|
||||
key={action.id}
|
||||
action={action}
|
||||
stateChanged={updateInActions}
|
||||
onDelete={() =>
|
||||
setActions((actions) =>
|
||||
actions.filter((a) => a.id !== action.id),
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
startIcon={<Add />}
|
||||
onClick={() => addAction(projectId)}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
>
|
||||
Add action
|
||||
</Button>
|
||||
</StyledButtonContainer>
|
||||
</ProjectActionsFormStep>
|
||||
<ProjectActionsFormStepActions
|
||||
serviceAccounts={serviceAccounts}
|
||||
serviceAccountsLoading={serviceAccountsLoading}
|
||||
actions={actions}
|
||||
setActions={setActions}
|
||||
actorId={actorId}
|
||||
setActorId={setActorId}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={showErrors}
|
||||
show={() => (
|
||||
<Alert severity='error' icon={false}>
|
||||
<StyledAlert severity='error' icon={false}>
|
||||
<ul>
|
||||
{Object.values(errors)
|
||||
.filter(Boolean)
|
||||
@ -331,7 +164,7 @@ export const ProjectActionsForm = ({
|
||||
<li key={error}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
</StyledAlert>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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 = (
|
||||
<>
|
||||
<span>Action {index + 1}</span>
|
||||
<div>
|
||||
<Tooltip title='Delete action' arrow>
|
||||
<IconButton onClick={onDelete}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ProjectActionsFormItem index={index} header={header} separator='THEN'>
|
||||
<StyledItemBody>
|
||||
<StyledItemRow>
|
||||
<StyledFieldContainer>
|
||||
<GeneralSelect
|
||||
label='Action'
|
||||
name='action'
|
||||
options={options}
|
||||
value={actionName}
|
||||
onChange={(selected) =>
|
||||
stateChanged({
|
||||
...action,
|
||||
action: selected,
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledFieldContainer>
|
||||
<StyledFieldContainer>
|
||||
<GeneralSelect
|
||||
label='Environment'
|
||||
name='environment'
|
||||
options={environments.map((environment) => ({
|
||||
label: environment,
|
||||
key: environment,
|
||||
}))}
|
||||
value={action.executionParams.environment as string}
|
||||
onChange={(selected) =>
|
||||
stateChanged({
|
||||
...action,
|
||||
executionParams: {
|
||||
...action.executionParams,
|
||||
environment: selected,
|
||||
},
|
||||
})
|
||||
}
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledFieldContainer>
|
||||
</StyledItemRow>
|
||||
<ConditionallyRender
|
||||
condition={!hasPermission}
|
||||
show={
|
||||
<Alert severity='error'>
|
||||
The selected service account does not have
|
||||
permissions to execute this action currently.
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
</StyledItemBody>
|
||||
</ProjectActionsFormItem>
|
||||
);
|
||||
};
|
@ -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<React.SetStateAction<ActionsActionState[]>>;
|
||||
actorId: number;
|
||||
setActorId: React.Dispatch<React.SetStateAction<number>>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<ProjectActionsFormStep
|
||||
name='Do these actions'
|
||||
verticalConnector
|
||||
resourceLink={
|
||||
<RouterLink to='/admin/service-accounts'>
|
||||
Create service account
|
||||
</RouterLink>
|
||||
}
|
||||
>
|
||||
<GeneralSelect
|
||||
label='Service account'
|
||||
name='service-account'
|
||||
options={serviceAccountOptions}
|
||||
value={`${actorId}`}
|
||||
onChange={(v) => {
|
||||
setActorId(parseInt(v));
|
||||
}}
|
||||
/>
|
||||
<StyledDivider />
|
||||
{actions.map((action, index) => (
|
||||
<ProjectActionsActionItem
|
||||
index={index}
|
||||
key={action.id}
|
||||
action={action}
|
||||
stateChanged={updateInActions}
|
||||
actorId={actorId}
|
||||
onDelete={() =>
|
||||
setActions((actions) =>
|
||||
actions.filter((a) => a.id !== action.id),
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
startIcon={<Add />}
|
||||
onClick={() => addAction(projectId)}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
>
|
||||
Add action
|
||||
</Button>
|
||||
</StyledButtonContainer>
|
||||
</ProjectActionsFormStep>
|
||||
);
|
||||
};
|
@ -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 = ({
|
||||
<StyledFilter>
|
||||
<StyledFilterHeader>
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
label='Parameter'
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
options={suggestions}
|
||||
value={parameter}
|
||||
onChange={(e) =>
|
||||
onInputChange={(_, parameter) =>
|
||||
stateChanged({
|
||||
...filter,
|
||||
parameter: e.target.value,
|
||||
parameter,
|
||||
})
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
size='small'
|
||||
label='Parameter'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
<StyledOperatorOptions>
|
@ -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<React.SetStateAction<number>>;
|
||||
filters: ActionsFilterState[];
|
||||
setFilters: React.Dispatch<React.SetStateAction<ActionsFilterState[]>>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<ProjectActionsFormStep
|
||||
name='When this'
|
||||
resourceLink={
|
||||
<RouterLink to='/integrations/signals'>
|
||||
Create signal endpoint
|
||||
</RouterLink>
|
||||
}
|
||||
>
|
||||
<GeneralSelect
|
||||
label='Source'
|
||||
options={signalEndpointOptions}
|
||||
value={`${sourceId}`}
|
||||
onChange={(v) => {
|
||||
setSourceId(parseInt(v));
|
||||
}}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(sourceId)}
|
||||
show={
|
||||
<ProjectActionsPreviewPayload payload={lastSourcePayload} />
|
||||
}
|
||||
/>
|
||||
<StyledDivider />
|
||||
{filters.map((filter, index) => (
|
||||
<ProjectActionsFilterItem
|
||||
key={filter.id}
|
||||
index={index}
|
||||
filter={filter}
|
||||
stateChanged={updateInFilters}
|
||||
suggestions={filterSuggestions}
|
||||
onDelete={() =>
|
||||
setFilters((filters) =>
|
||||
filters.filter(({ id }) => id !== filter.id),
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
startIcon={<Add />}
|
||||
onClick={addFilter}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
>
|
||||
Add filter
|
||||
</Button>
|
||||
<HelpIcon
|
||||
htmlTooltip
|
||||
tooltip={
|
||||
<StyledTooltip>
|
||||
<p>
|
||||
Filters allow you to add conditions to the
|
||||
execution of the actions based on the source
|
||||
payload.
|
||||
</p>
|
||||
<p>
|
||||
If no filters are defined then the action will
|
||||
always be triggered from the selected source, no
|
||||
matter the payload.
|
||||
</p>
|
||||
</StyledTooltip>
|
||||
}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</ProjectActionsFormStep>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<StyledNoSignalsSpan>
|
||||
No signals were received from this source yet.
|
||||
</StyledNoSignalsSpan>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledAccordion>
|
||||
<StyledAccordionSummary
|
||||
expandIcon={
|
||||
<IconButton>
|
||||
<StyledArrowForwardIosSharp titleAccess='Toggle' />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
Preview payload
|
||||
</StyledAccordionSummary>
|
||||
<StyledAccordionDetails>
|
||||
<Suspense fallback={null}>
|
||||
<LazyReactJSONEditor
|
||||
content={{ json: payload }}
|
||||
readOnly
|
||||
statusBar={false}
|
||||
editorStyle='sidePanel'
|
||||
/>
|
||||
</Suspense>
|
||||
</StyledAccordionDetails>
|
||||
</StyledAccordion>
|
||||
);
|
||||
};
|
@ -194,6 +194,8 @@ export const ProjectActionsModal = ({
|
||||
setEnabled={setEnabled}
|
||||
name={name}
|
||||
setName={setName}
|
||||
description={description}
|
||||
setDescription={setDescription}
|
||||
sourceId={sourceId}
|
||||
setSourceId={setSourceId}
|
||||
filters={filters}
|
||||
|
@ -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),
|
||||
|
@ -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<IServiceAccountAccessMatrixResponse> {
|
||||
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<IServiceAccountAccessMatrixResponse>(
|
||||
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());
|
||||
};
|
Loading…
Reference in New Issue
Block a user