diff --git a/frontend/src/component/admin/groups/CreateGroup/CreateGroup.tsx b/frontend/src/component/admin/groups/CreateGroup/CreateGroup.tsx index 12661a7d51..83a83ca4ce 100644 --- a/frontend/src/component/admin/groups/CreateGroup/CreateGroup.tsx +++ b/frontend/src/component/admin/groups/CreateGroup/CreateGroup.tsx @@ -22,6 +22,8 @@ export const CreateGroup = () => { setName, description, setDescription, + mappingsSSO, + setMappingsSSO, users, setUsers, getGroupPayload, @@ -92,9 +94,11 @@ export const CreateGroup = () => { { 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(); @@ -96,9 +103,11 @@ export const EditGroup = () => { = ({ const { users, setUsers, getGroupPayload } = useGroupForm( group.name, group.description, + group.mappingsSSO, group.users ); diff --git a/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx b/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx index 704583770b..1b04ff4247 100644 --- a/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx +++ b/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx @@ -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', @@ -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), })); @@ -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>; + setMappingsSSO: React.Dispatch>; setUsers: React.Dispatch>; handleSubmit: (e: any) => void; handleCancel: () => void; @@ -54,71 +64,92 @@ interface IGroupForm { export const GroupForm: FC = ({ name, description, + mappingsSSO, users, setName, setDescription, + setMappingsSSO, setUsers, handleSubmit, handleCancel, errors, mode, children, -}) => ( - -
- - What would you like to call your group? - - setName(e.target.value)} - data-testid={UG_NAME_ID} - required - /> - - How would you describe your group? - - setDescription(e.target.value)} - data-testid={UG_DESC_ID} - /> - - - Add users to this group - - - - { + const { uiConfig } = useUiConfig(); + + return ( + +
+ + What would you like to call your group? + + setName(e.target.value)} + data-testid={UG_NAME_ID} + required + /> + + How would you describe your group? + + setDescription(e.target.value)} + data-testid={UG_DESC_ID} + /> + + + Is this group associated with SSO groups? + + + + } + /> + + + Add users to this group + + - - - } - /> -
+ + + + + } + /> +
- - {children} - - Cancel - - -
-); + + {children} + + Cancel + + + + ); +}; diff --git a/frontend/src/component/admin/groups/hooks/useGroupForm.ts b/frontend/src/component/admin/groups/hooks/useGroupForm.ts index da4b5c4adf..826b9a3ad5 100644 --- a/frontend/src/component/admin/groups/hooks/useGroupForm.ts +++ b/frontend/src/component/admin/groups/hooks/useGroupForm.ts @@ -5,12 +5,14 @@ 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(initialUsers); const [errors, setErrors] = useState({}); @@ -18,6 +20,7 @@ export const useGroupForm = ( return { name, description, + mappingsSSO, users: users.map(({ id }) => ({ user: { id }, })), @@ -33,6 +36,8 @@ export const useGroupForm = ( setName, description, setDescription, + mappingsSSO, + setMappingsSSO, users, setUsers, getGroupPayload, diff --git a/frontend/src/component/common/ItemList/ItemList.tsx b/frontend/src/component/common/ItemList/ItemList.tsx new file mode 100644 index 0000000000..d1a0360914 --- /dev/null +++ b/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>; +} + +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 ( +
+ + setInputValue(e.target.value)} + onKeyPress={e => { + if (e.key === 'Enter') { + addItem(); + } + }} + /> + + + + {value?.map((item, index) => ( + removeItem(item)} + /> + ))} + +
+ ); +}; diff --git a/frontend/src/hooks/api/actions/useGroupApi/useGroupApi.ts b/frontend/src/hooks/api/actions/useGroupApi/useGroupApi.ts index 8f50fc1147..973dfb2618 100644 --- a/frontend/src/hooks/api/actions/useGroupApi/useGroupApi.ts +++ b/frontend/src/hooks/api/actions/useGroupApi/useGroupApi.ts @@ -4,6 +4,7 @@ import { IGroupUserModel } from 'interfaces/group'; interface ICreateGroupPayload { name: string; description: string; + mappingsSSO: string[]; users: IGroupUserModel[]; } diff --git a/frontend/src/interfaces/group.ts b/frontend/src/interfaces/group.ts index 95b9d820a7..dac0351698 100644 --- a/frontend/src/interfaces/group.ts +++ b/frontend/src/interfaces/group.ts @@ -9,6 +9,7 @@ export interface IGroup { projects: string[]; addedAt?: string; userCount?: number; + mappingsSSO: string[]; } export interface IGroupUser extends IUser { diff --git a/src/lib/db/group-store.ts b/src/lib/db/group-store.ts index 7e33f480e7..476e9e43d7 100644 --- a/src/lib/db/group-store.ts +++ b/src/lib/db/group-store.ts @@ -20,7 +20,14 @@ const T = { ROLES: 'roles', }; -const GROUP_COLUMNS = ['id', 'name', 'description', 'created_at', 'created_by']; +const GROUP_COLUMNS = [ + 'id', + 'name', + 'description', + 'mappings_sso', + 'created_at', + 'created_by', +]; const rowToGroup = (row) => { if (!row) { @@ -30,6 +37,7 @@ const rowToGroup = (row) => { id: row.id, name: row.name, description: row.description, + mappingsSSO: row.mappings_sso, createdAt: row.created_at, createdBy: row.created_by, }); @@ -46,9 +54,10 @@ const rowToGroupUser = (row) => { }; }; -const groupToRow = (user: IStoreGroup) => ({ - name: user.name, - description: user.description, +const groupToRow = (group: IStoreGroup) => ({ + name: group.name, + description: group.description, + mappings_sso: JSON.stringify(group.mappingsSSO), }); export default class GroupStore implements IGroupStore { @@ -69,10 +78,7 @@ export default class GroupStore implements IGroupStore { async update(group: IGroupModel): Promise { const rows = await this.db(T.GROUPS) .where({ id: group.id }) - .update({ - name: group.name, - description: group.description, - }) + .update(groupToRow(group)) .returning(GROUP_COLUMNS); return rowToGroup(rows[0]); diff --git a/src/lib/openapi/spec/group-schema.ts b/src/lib/openapi/spec/group-schema.ts index de209b1083..78d507eff8 100644 --- a/src/lib/openapi/spec/group-schema.ts +++ b/src/lib/openapi/spec/group-schema.ts @@ -17,6 +17,12 @@ export const groupSchema = { description: { type: 'string', }, + mappingsSSO: { + type: 'array', + items: { + type: 'string', + }, + }, createdBy: { type: 'string', nullable: true, diff --git a/src/lib/types/group.ts b/src/lib/types/group.ts index f107c9235e..4c83a774d6 100644 --- a/src/lib/types/group.ts +++ b/src/lib/types/group.ts @@ -5,6 +5,7 @@ export interface IGroup { id?: number; name: string; description?: string; + mappingsSSO?: string[]; createdAt?: Date; userCount?: number; createdBy?: string; @@ -57,7 +58,16 @@ export default class Group implements IGroup { description: string; - constructor({ id, name, description, createdBy, createdAt }: IGroup) { + mappingsSSO: string[]; + + constructor({ + id, + name, + description, + mappingsSSO, + createdBy, + createdAt, + }: IGroup) { if (!id) { throw new TypeError('Id is required'); } @@ -67,6 +77,7 @@ export default class Group implements IGroup { this.id = id; this.name = name; this.description = description; + this.mappingsSSO = mappingsSSO; this.createdBy = createdBy; this.createdAt = createdAt; } diff --git a/src/lib/types/stores/group-store.ts b/src/lib/types/stores/group-store.ts index f4e9ec1664..604901833d 100644 --- a/src/lib/types/stores/group-store.ts +++ b/src/lib/types/stores/group-store.ts @@ -11,6 +11,7 @@ import { export interface IStoreGroup { name: string; description?: string; + mappingsSSO?: string[]; } export interface IGroupStore extends Store { diff --git a/src/migrations/20221011155007-add-user-groups-mappings.js b/src/migrations/20221011155007-add-user-groups-mappings.js new file mode 100644 index 0000000000..7631575dfa --- /dev/null +++ b/src/migrations/20221011155007-add-user-groups-mappings.js @@ -0,0 +1,21 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + ALTER TABLE groups + ADD COLUMN IF NOT EXISTS mappings_sso jsonb DEFAULT '[]'::jsonb + `, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + ALTER TABLE groups + DROP COLUMN IF EXISTS mappings_sso; + `, + cb, + ); +}; diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 2dafdf2cc1..35dace52bd 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -1390,6 +1390,12 @@ exports[`should serve the OpenAPI spec 1`] = ` "id": { "type": "number", }, + "mappingsSSO": { + "items": { + "type": "string", + }, + "type": "array", + }, "name": { "type": "string", },