Skip to content

Commit

Permalink
feat: add SSO mappings to groups (#2175)
Browse files Browse the repository at this point in the history
* feat: add SSO mappings to groups

* add feature flag to conditionally render

* fix EditGroupUsers

* fix: update snap
  • Loading branch information
nunogois committed Oct 13, 2022
1 parent b1a877e commit a3bf564
Show file tree
Hide file tree
Showing 14 changed files with 243 additions and 65 deletions.
Expand Up @@ -22,6 +22,8 @@ export const CreateGroup = () => {
setName,
description,
setDescription,
mappingsSSO,
setMappingsSSO,
users,
setUsers,
getGroupPayload,
Expand Down Expand Up @@ -92,9 +94,11 @@ export const CreateGroup = () => {
<GroupForm
name={name}
description={description}
mappingsSSO={mappingsSSO}
users={users}
setName={onSetName}
setDescription={setDescription}
setMappingsSSO={setMappingsSSO}
setUsers={setUsers}
errors={errors}
handleSubmit={handleSubmit}
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/component/admin/groups/EditGroup/EditGroup.tsx
Expand Up @@ -27,13 +27,20 @@ export const EditGroup = () => {
setName,
description,
setDescription,
mappingsSSO,
setMappingsSSO,
users,
setUsers,
getGroupPayload,
clearErrors,
errors,
setErrors,
} = useGroupForm(group?.name, group?.description, group?.users);
} = useGroupForm(
group?.name,
group?.description,
group?.mappingsSSO,
group?.users
);

const { groups } = useGroups();
const { updateGroup, loading } = useGroupApi();
Expand Down Expand Up @@ -96,9 +103,11 @@ export const EditGroup = () => {
<GroupForm
name={name}
description={description}
mappingsSSO={mappingsSSO}
users={users}
setName={onSetName}
setDescription={setDescription}
setMappingsSSO={setMappingsSSO}
setUsers={setUsers}
errors={errors}
handleSubmit={handleSubmit}
Expand Down
Expand Up @@ -55,6 +55,7 @@ export const EditGroupUsers: FC<IEditGroupUsersProps> = ({
const { users, setUsers, getGroupPayload } = useGroupForm(
group.name,
group.description,
group.mappingsSSO,
group.users
);

Expand Down
141 changes: 86 additions & 55 deletions frontend/src/component/admin/groups/GroupForm/GroupForm.tsx
Expand Up @@ -6,6 +6,8 @@ import { IGroupUser } from 'interfaces/group';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { GroupFormUsersSelect } from './GroupFormUsersSelect/GroupFormUsersSelect';
import { GroupFormUsersTable } from './GroupFormUsersTable/GroupFormUsersTable';
import { ItemList } from 'component/common/ItemList/ItemList';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';

const StyledForm = styled('form')(() => ({
display: 'flex',
Expand All @@ -24,6 +26,12 @@ const StyledInput = styled(Input)(({ theme }) => ({
marginBottom: theme.spacing(2),
}));

const StyledItemList = styled(ItemList)(({ theme }) => ({
width: '100%',
maxWidth: theme.spacing(50),
marginBottom: theme.spacing(2),
}));

const StyledGroupFormUsersTableWrapper = styled('div')(({ theme }) => ({
marginBottom: theme.spacing(6),
}));
Expand All @@ -41,9 +49,11 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
interface IGroupForm {
name: string;
description: string;
mappingsSSO: string[];
users: IGroupUser[];
setName: (name: string) => void;
setDescription: React.Dispatch<React.SetStateAction<string>>;
setMappingsSSO: React.Dispatch<React.SetStateAction<string[]>>;
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
handleSubmit: (e: any) => void;
handleCancel: () => void;
Expand All @@ -54,71 +64,92 @@ interface IGroupForm {
export const GroupForm: FC<IGroupForm> = ({
name,
description,
mappingsSSO,
users,
setName,
setDescription,
setMappingsSSO,
setUsers,
handleSubmit,
handleCancel,
errors,
mode,
children,
}) => (
<StyledForm onSubmit={handleSubmit}>
<div>
<StyledInputDescription>
What would you like to call your group?
</StyledInputDescription>
<StyledInput
autoFocus
label="Name"
id="group-name"
error={Boolean(errors.name)}
errorText={errors.name}
value={name}
onChange={e => setName(e.target.value)}
data-testid={UG_NAME_ID}
required
/>
<StyledInputDescription>
How would you describe your group?
</StyledInputDescription>
<StyledInput
multiline
rows={4}
label="Description"
placeholder="A short description of the group"
value={description}
onChange={e => setDescription(e.target.value)}
data-testid={UG_DESC_ID}
/>
<ConditionallyRender
condition={mode === 'Create'}
show={
<>
<StyledInputDescription>
Add users to this group
</StyledInputDescription>
<GroupFormUsersSelect
users={users}
setUsers={setUsers}
/>
<StyledGroupFormUsersTableWrapper>
<GroupFormUsersTable
}) => {
const { uiConfig } = useUiConfig();

return (
<StyledForm onSubmit={handleSubmit}>
<div>
<StyledInputDescription>
What would you like to call your group?
</StyledInputDescription>
<StyledInput
autoFocus
label="Name"
id="group-name"
error={Boolean(errors.name)}
errorText={errors.name}
value={name}
onChange={e => setName(e.target.value)}
data-testid={UG_NAME_ID}
required
/>
<StyledInputDescription>
How would you describe your group?
</StyledInputDescription>
<StyledInput
multiline
rows={4}
label="Description"
placeholder="A short description of the group"
value={description}
onChange={e => setDescription(e.target.value)}
data-testid={UG_DESC_ID}
/>
<ConditionallyRender
condition={Boolean(uiConfig.flags.syncSSOGroups)}
show={
<>
<StyledInputDescription>
Is this group associated with SSO groups?
</StyledInputDescription>
<StyledItemList
label="SSO group ID / name"
value={mappingsSSO}
onChange={setMappingsSSO}
/>
</>
}
/>
<ConditionallyRender
condition={mode === 'Create'}
show={
<>
<StyledInputDescription>
Add users to this group
</StyledInputDescription>
<GroupFormUsersSelect
users={users}
setUsers={setUsers}
/>
</StyledGroupFormUsersTableWrapper>
</>
}
/>
</div>
<StyledGroupFormUsersTableWrapper>
<GroupFormUsersTable
users={users}
setUsers={setUsers}
/>
</StyledGroupFormUsersTableWrapper>
</>
}
/>
</div>

<StyledButtonContainer>
{children}
<StyledCancelButton onClick={handleCancel}>
Cancel
</StyledCancelButton>
</StyledButtonContainer>
</StyledForm>
);
<StyledButtonContainer>
{children}
<StyledCancelButton onClick={handleCancel}>
Cancel
</StyledCancelButton>
</StyledButtonContainer>
</StyledForm>
);
};
5 changes: 5 additions & 0 deletions frontend/src/component/admin/groups/hooks/useGroupForm.ts
Expand Up @@ -5,19 +5,22 @@ import { IGroupUser } from 'interfaces/group';
export const useGroupForm = (
initialName = '',
initialDescription = '',
initialMappingsSSO: string[] = [],
initialUsers: IGroupUser[] = []
) => {
const params = useQueryParams();
const groupQueryName = params.get('name');
const [name, setName] = useState(groupQueryName || initialName);
const [description, setDescription] = useState(initialDescription);
const [mappingsSSO, setMappingsSSO] = useState(initialMappingsSSO);
const [users, setUsers] = useState<IGroupUser[]>(initialUsers);
const [errors, setErrors] = useState({});

const getGroupPayload = () => {
return {
name,
description,
mappingsSSO,
users: users.map(({ id }) => ({
user: { id },
})),
Expand All @@ -33,6 +36,8 @@ export const useGroupForm = (
setName,
description,
setDescription,
mappingsSSO,
setMappingsSSO,
users,
setUsers,
getGroupPayload,
Expand Down
75 changes: 75 additions & 0 deletions frontend/src/component/common/ItemList/ItemList.tsx
@@ -0,0 +1,75 @@
import { Add } from '@mui/icons-material';
import { Button, Chip, Stack, styled } from '@mui/material';
import Input from 'component/common/Input/Input';
import { useState } from 'react';

const StyledItemListAdd = styled('div')(({ theme }) => ({
display: 'flex',
marginBottom: theme.spacing(1),
'& > div:first-of-type': {
width: '100%',
marginRight: theme.spacing(1),
'& > div:first-of-type': {
width: '100%',
},
},
}));

interface IItemListProps {
label: string;
value: string[];
onChange: React.Dispatch<React.SetStateAction<string[]>>;
}

export const ItemList = ({
label,
value,
onChange,
...props
}: IItemListProps) => {
const [inputValue, setInputValue] = useState('');

const addItem = () => {
onChange(prev => [...prev, inputValue]);
setInputValue('');
};

const removeItem = (value: string) => {
onChange(prev => prev.filter(item => item !== value));
};

return (
<div {...props}>
<StyledItemListAdd>
<Input
label={label}
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onKeyPress={e => {
if (e.key === 'Enter') {
addItem();
}
}}
/>
<Button
startIcon={<Add />}
onClick={addItem}
variant="outlined"
color="primary"
disabled={!inputValue.trim() || value.includes(inputValue)}
>
Add
</Button>
</StyledItemListAdd>
<Stack flexDirection="row" flexWrap={'wrap'} gap={1}>
{value?.map((item, index) => (
<Chip
key={index}
label={item}
onDelete={() => removeItem(item)}
/>
))}
</Stack>
</div>
);
};
1 change: 1 addition & 0 deletions frontend/src/hooks/api/actions/useGroupApi/useGroupApi.ts
Expand Up @@ -4,6 +4,7 @@ import { IGroupUserModel } from 'interfaces/group';
interface ICreateGroupPayload {
name: string;
description: string;
mappingsSSO: string[];
users: IGroupUserModel[];
}

Expand Down
1 change: 1 addition & 0 deletions frontend/src/interfaces/group.ts
Expand Up @@ -9,6 +9,7 @@ export interface IGroup {
projects: string[];
addedAt?: string;
userCount?: number;
mappingsSSO: string[];
}

export interface IGroupUser extends IUser {
Expand Down

0 comments on commit a3bf564

Please sign in to comment.