mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
feat: add SSO mappings to groups (#2175)
* feat: add SSO mappings to groups * add feature flag to conditionally render * fix EditGroupUsers * fix: update snap
This commit is contained in:
parent
b1a877e56c
commit
a3bf564100
@ -22,6 +22,8 @@ export const CreateGroup = () => {
|
|||||||
setName,
|
setName,
|
||||||
description,
|
description,
|
||||||
setDescription,
|
setDescription,
|
||||||
|
mappingsSSO,
|
||||||
|
setMappingsSSO,
|
||||||
users,
|
users,
|
||||||
setUsers,
|
setUsers,
|
||||||
getGroupPayload,
|
getGroupPayload,
|
||||||
@ -92,9 +94,11 @@ export const CreateGroup = () => {
|
|||||||
<GroupForm
|
<GroupForm
|
||||||
name={name}
|
name={name}
|
||||||
description={description}
|
description={description}
|
||||||
|
mappingsSSO={mappingsSSO}
|
||||||
users={users}
|
users={users}
|
||||||
setName={onSetName}
|
setName={onSetName}
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
|
setMappingsSSO={setMappingsSSO}
|
||||||
setUsers={setUsers}
|
setUsers={setUsers}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
|
@ -27,13 +27,20 @@ export const EditGroup = () => {
|
|||||||
setName,
|
setName,
|
||||||
description,
|
description,
|
||||||
setDescription,
|
setDescription,
|
||||||
|
mappingsSSO,
|
||||||
|
setMappingsSSO,
|
||||||
users,
|
users,
|
||||||
setUsers,
|
setUsers,
|
||||||
getGroupPayload,
|
getGroupPayload,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
errors,
|
errors,
|
||||||
setErrors,
|
setErrors,
|
||||||
} = useGroupForm(group?.name, group?.description, group?.users);
|
} = useGroupForm(
|
||||||
|
group?.name,
|
||||||
|
group?.description,
|
||||||
|
group?.mappingsSSO,
|
||||||
|
group?.users
|
||||||
|
);
|
||||||
|
|
||||||
const { groups } = useGroups();
|
const { groups } = useGroups();
|
||||||
const { updateGroup, loading } = useGroupApi();
|
const { updateGroup, loading } = useGroupApi();
|
||||||
@ -96,9 +103,11 @@ export const EditGroup = () => {
|
|||||||
<GroupForm
|
<GroupForm
|
||||||
name={name}
|
name={name}
|
||||||
description={description}
|
description={description}
|
||||||
|
mappingsSSO={mappingsSSO}
|
||||||
users={users}
|
users={users}
|
||||||
setName={onSetName}
|
setName={onSetName}
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
|
setMappingsSSO={setMappingsSSO}
|
||||||
setUsers={setUsers}
|
setUsers={setUsers}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
|
@ -55,6 +55,7 @@ export const EditGroupUsers: FC<IEditGroupUsersProps> = ({
|
|||||||
const { users, setUsers, getGroupPayload } = useGroupForm(
|
const { users, setUsers, getGroupPayload } = useGroupForm(
|
||||||
group.name,
|
group.name,
|
||||||
group.description,
|
group.description,
|
||||||
|
group.mappingsSSO,
|
||||||
group.users
|
group.users
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ import { IGroupUser } from 'interfaces/group';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { GroupFormUsersSelect } from './GroupFormUsersSelect/GroupFormUsersSelect';
|
import { GroupFormUsersSelect } from './GroupFormUsersSelect/GroupFormUsersSelect';
|
||||||
import { GroupFormUsersTable } from './GroupFormUsersTable/GroupFormUsersTable';
|
import { GroupFormUsersTable } from './GroupFormUsersTable/GroupFormUsersTable';
|
||||||
|
import { ItemList } from 'component/common/ItemList/ItemList';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const StyledForm = styled('form')(() => ({
|
const StyledForm = styled('form')(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -24,6 +26,12 @@ const StyledInput = styled(Input)(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledItemList = styled(ItemList)(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: theme.spacing(50),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
const StyledGroupFormUsersTableWrapper = styled('div')(({ theme }) => ({
|
const StyledGroupFormUsersTableWrapper = styled('div')(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(6),
|
marginBottom: theme.spacing(6),
|
||||||
}));
|
}));
|
||||||
@ -41,9 +49,11 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
|
|||||||
interface IGroupForm {
|
interface IGroupForm {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
mappingsSSO: string[];
|
||||||
users: IGroupUser[];
|
users: IGroupUser[];
|
||||||
setName: (name: string) => void;
|
setName: (name: string) => void;
|
||||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setMappingsSSO: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
@ -54,71 +64,92 @@ interface IGroupForm {
|
|||||||
export const GroupForm: FC<IGroupForm> = ({
|
export const GroupForm: FC<IGroupForm> = ({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
mappingsSSO,
|
||||||
users,
|
users,
|
||||||
setName,
|
setName,
|
||||||
setDescription,
|
setDescription,
|
||||||
|
setMappingsSSO,
|
||||||
setUsers,
|
setUsers,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
errors,
|
errors,
|
||||||
mode,
|
mode,
|
||||||
children,
|
children,
|
||||||
}) => (
|
}) => {
|
||||||
<StyledForm onSubmit={handleSubmit}>
|
const { uiConfig } = useUiConfig();
|
||||||
<div>
|
|
||||||
<StyledInputDescription>
|
return (
|
||||||
What would you like to call your group?
|
<StyledForm onSubmit={handleSubmit}>
|
||||||
</StyledInputDescription>
|
<div>
|
||||||
<StyledInput
|
<StyledInputDescription>
|
||||||
autoFocus
|
What would you like to call your group?
|
||||||
label="Name"
|
</StyledInputDescription>
|
||||||
id="group-name"
|
<StyledInput
|
||||||
error={Boolean(errors.name)}
|
autoFocus
|
||||||
errorText={errors.name}
|
label="Name"
|
||||||
value={name}
|
id="group-name"
|
||||||
onChange={e => setName(e.target.value)}
|
error={Boolean(errors.name)}
|
||||||
data-testid={UG_NAME_ID}
|
errorText={errors.name}
|
||||||
required
|
value={name}
|
||||||
/>
|
onChange={e => setName(e.target.value)}
|
||||||
<StyledInputDescription>
|
data-testid={UG_NAME_ID}
|
||||||
How would you describe your group?
|
required
|
||||||
</StyledInputDescription>
|
/>
|
||||||
<StyledInput
|
<StyledInputDescription>
|
||||||
multiline
|
How would you describe your group?
|
||||||
rows={4}
|
</StyledInputDescription>
|
||||||
label="Description"
|
<StyledInput
|
||||||
placeholder="A short description of the group"
|
multiline
|
||||||
value={description}
|
rows={4}
|
||||||
onChange={e => setDescription(e.target.value)}
|
label="Description"
|
||||||
data-testid={UG_DESC_ID}
|
placeholder="A short description of the group"
|
||||||
/>
|
value={description}
|
||||||
<ConditionallyRender
|
onChange={e => setDescription(e.target.value)}
|
||||||
condition={mode === 'Create'}
|
data-testid={UG_DESC_ID}
|
||||||
show={
|
/>
|
||||||
<>
|
<ConditionallyRender
|
||||||
<StyledInputDescription>
|
condition={Boolean(uiConfig.flags.syncSSOGroups)}
|
||||||
Add users to this group
|
show={
|
||||||
</StyledInputDescription>
|
<>
|
||||||
<GroupFormUsersSelect
|
<StyledInputDescription>
|
||||||
users={users}
|
Is this group associated with SSO groups?
|
||||||
setUsers={setUsers}
|
</StyledInputDescription>
|
||||||
/>
|
<StyledItemList
|
||||||
<StyledGroupFormUsersTableWrapper>
|
label="SSO group ID / name"
|
||||||
<GroupFormUsersTable
|
value={mappingsSSO}
|
||||||
|
onChange={setMappingsSSO}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={mode === 'Create'}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StyledInputDescription>
|
||||||
|
Add users to this group
|
||||||
|
</StyledInputDescription>
|
||||||
|
<GroupFormUsersSelect
|
||||||
users={users}
|
users={users}
|
||||||
setUsers={setUsers}
|
setUsers={setUsers}
|
||||||
/>
|
/>
|
||||||
</StyledGroupFormUsersTableWrapper>
|
<StyledGroupFormUsersTableWrapper>
|
||||||
</>
|
<GroupFormUsersTable
|
||||||
}
|
users={users}
|
||||||
/>
|
setUsers={setUsers}
|
||||||
</div>
|
/>
|
||||||
|
</StyledGroupFormUsersTableWrapper>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
{children}
|
{children}
|
||||||
<StyledCancelButton onClick={handleCancel}>
|
<StyledCancelButton onClick={handleCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</StyledCancelButton>
|
</StyledCancelButton>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
</StyledForm>
|
</StyledForm>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
@ -5,12 +5,14 @@ import { IGroupUser } from 'interfaces/group';
|
|||||||
export const useGroupForm = (
|
export const useGroupForm = (
|
||||||
initialName = '',
|
initialName = '',
|
||||||
initialDescription = '',
|
initialDescription = '',
|
||||||
|
initialMappingsSSO: string[] = [],
|
||||||
initialUsers: IGroupUser[] = []
|
initialUsers: IGroupUser[] = []
|
||||||
) => {
|
) => {
|
||||||
const params = useQueryParams();
|
const params = useQueryParams();
|
||||||
const groupQueryName = params.get('name');
|
const groupQueryName = params.get('name');
|
||||||
const [name, setName] = useState(groupQueryName || initialName);
|
const [name, setName] = useState(groupQueryName || initialName);
|
||||||
const [description, setDescription] = useState(initialDescription);
|
const [description, setDescription] = useState(initialDescription);
|
||||||
|
const [mappingsSSO, setMappingsSSO] = useState(initialMappingsSSO);
|
||||||
const [users, setUsers] = useState<IGroupUser[]>(initialUsers);
|
const [users, setUsers] = useState<IGroupUser[]>(initialUsers);
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
@ -18,6 +20,7 @@ export const useGroupForm = (
|
|||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
mappingsSSO,
|
||||||
users: users.map(({ id }) => ({
|
users: users.map(({ id }) => ({
|
||||||
user: { id },
|
user: { id },
|
||||||
})),
|
})),
|
||||||
@ -33,6 +36,8 @@ export const useGroupForm = (
|
|||||||
setName,
|
setName,
|
||||||
description,
|
description,
|
||||||
setDescription,
|
setDescription,
|
||||||
|
mappingsSSO,
|
||||||
|
setMappingsSSO,
|
||||||
users,
|
users,
|
||||||
setUsers,
|
setUsers,
|
||||||
getGroupPayload,
|
getGroupPayload,
|
||||||
|
75
frontend/src/component/common/ItemList/ItemList.tsx
Normal file
75
frontend/src/component/common/ItemList/ItemList.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -4,6 +4,7 @@ import { IGroupUserModel } from 'interfaces/group';
|
|||||||
interface ICreateGroupPayload {
|
interface ICreateGroupPayload {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
mappingsSSO: string[];
|
||||||
users: IGroupUserModel[];
|
users: IGroupUserModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ export interface IGroup {
|
|||||||
projects: string[];
|
projects: string[];
|
||||||
addedAt?: string;
|
addedAt?: string;
|
||||||
userCount?: number;
|
userCount?: number;
|
||||||
|
mappingsSSO: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGroupUser extends IUser {
|
export interface IGroupUser extends IUser {
|
||||||
|
@ -20,7 +20,14 @@ const T = {
|
|||||||
ROLES: 'roles',
|
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) => {
|
const rowToGroup = (row) => {
|
||||||
if (!row) {
|
if (!row) {
|
||||||
@ -30,6 +37,7 @@ const rowToGroup = (row) => {
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
|
mappingsSSO: row.mappings_sso,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
createdBy: row.created_by,
|
createdBy: row.created_by,
|
||||||
});
|
});
|
||||||
@ -46,9 +54,10 @@ const rowToGroupUser = (row) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const groupToRow = (user: IStoreGroup) => ({
|
const groupToRow = (group: IStoreGroup) => ({
|
||||||
name: user.name,
|
name: group.name,
|
||||||
description: user.description,
|
description: group.description,
|
||||||
|
mappings_sso: JSON.stringify(group.mappingsSSO),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class GroupStore implements IGroupStore {
|
export default class GroupStore implements IGroupStore {
|
||||||
@ -69,10 +78,7 @@ export default class GroupStore implements IGroupStore {
|
|||||||
async update(group: IGroupModel): Promise<IGroup> {
|
async update(group: IGroupModel): Promise<IGroup> {
|
||||||
const rows = await this.db(T.GROUPS)
|
const rows = await this.db(T.GROUPS)
|
||||||
.where({ id: group.id })
|
.where({ id: group.id })
|
||||||
.update({
|
.update(groupToRow(group))
|
||||||
name: group.name,
|
|
||||||
description: group.description,
|
|
||||||
})
|
|
||||||
.returning(GROUP_COLUMNS);
|
.returning(GROUP_COLUMNS);
|
||||||
|
|
||||||
return rowToGroup(rows[0]);
|
return rowToGroup(rows[0]);
|
||||||
|
@ -17,6 +17,12 @@ export const groupSchema = {
|
|||||||
description: {
|
description: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
mappingsSSO: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
createdBy: {
|
createdBy: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -5,6 +5,7 @@ export interface IGroup {
|
|||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
mappingsSSO?: string[];
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
userCount?: number;
|
userCount?: number;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
@ -57,7 +58,16 @@ export default class Group implements IGroup {
|
|||||||
|
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
constructor({ id, name, description, createdBy, createdAt }: IGroup) {
|
mappingsSSO: string[];
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
mappingsSSO,
|
||||||
|
createdBy,
|
||||||
|
createdAt,
|
||||||
|
}: IGroup) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new TypeError('Id is required');
|
throw new TypeError('Id is required');
|
||||||
}
|
}
|
||||||
@ -67,6 +77,7 @@ export default class Group implements IGroup {
|
|||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
|
this.mappingsSSO = mappingsSSO;
|
||||||
this.createdBy = createdBy;
|
this.createdBy = createdBy;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
export interface IStoreGroup {
|
export interface IStoreGroup {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
mappingsSSO?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGroupStore extends Store<IGroup, number> {
|
export interface IGroupStore extends Store<IGroup, number> {
|
||||||
|
21
src/migrations/20221011155007-add-user-groups-mappings.js
Normal file
21
src/migrations/20221011155007-add-user-groups-mappings.js
Normal file
@ -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,
|
||||||
|
);
|
||||||
|
};
|
@ -1390,6 +1390,12 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
},
|
},
|
||||||
|
"mappingsSSO": {
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user