mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
UX additions to groups SSO syncing (#2200)
* Initial commit * Fix snapshot * Fixes * Small fix Co-authored-by: sjaanus <sellinjaanus@gmail.com>
This commit is contained in:
parent
f713190e34
commit
726674ea3e
@ -111,6 +111,14 @@ export const Group: VFC = () => {
|
|||||||
sortType: 'date',
|
sortType: 'date',
|
||||||
maxWidth: 150,
|
maxWidth: 150,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'createdBy',
|
||||||
|
Header: 'Added by',
|
||||||
|
accessor: 'createdBy',
|
||||||
|
Cell: HighlightCell,
|
||||||
|
minWidth: 90,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Header: 'Last login',
|
Header: 'Last login',
|
||||||
accessor: (row: IGroupUser) => row.seenAt || '',
|
accessor: (row: IGroupUser) => row.seenAt || '',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Button, styled } from '@mui/material';
|
import { Button, styled, Tooltip } from '@mui/material';
|
||||||
import { UG_DESC_ID, UG_NAME_ID } from 'utils/testIds';
|
import { UG_DESC_ID, UG_NAME_ID } from 'utils/testIds';
|
||||||
import Input from 'component/common/Input/Input';
|
import Input from 'component/common/Input/Input';
|
||||||
import { IGroupUser } from 'interfaces/group';
|
import { IGroupUser } from 'interfaces/group';
|
||||||
@ -8,6 +8,9 @@ import { GroupFormUsersSelect } from './GroupFormUsersSelect/GroupFormUsersSelec
|
|||||||
import { GroupFormUsersTable } from './GroupFormUsersTable/GroupFormUsersTable';
|
import { GroupFormUsersTable } from './GroupFormUsersTable/GroupFormUsersTable';
|
||||||
import { ItemList } from 'component/common/ItemList/ItemList';
|
import { ItemList } from 'component/common/ItemList/ItemList';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
|
||||||
|
import { HelpOutline } from '@mui/icons-material';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const StyledForm = styled('form')(() => ({
|
const StyledForm = styled('form')(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -22,13 +25,13 @@ const StyledInputDescription = styled('p')(({ theme }) => ({
|
|||||||
|
|
||||||
const StyledInput = styled(Input)(({ theme }) => ({
|
const StyledInput = styled(Input)(({ theme }) => ({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: theme.spacing(50),
|
maxWidth: theme.spacing(60),
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledItemList = styled(ItemList)(({ theme }) => ({
|
const StyledItemList = styled(ItemList)(({ theme }) => ({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: theme.spacing(50),
|
maxWidth: theme.spacing(60),
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -46,6 +49,22 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
|
|||||||
marginLeft: theme.spacing(3),
|
marginLeft: theme.spacing(3),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledDescriptionBlock = styled('div')(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: theme.spacing(60),
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
backgroundColor: theme.palette.neutral.light,
|
||||||
|
color: theme.palette.grey[900],
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledHelpOutline = styled(HelpOutline)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
marginLeft: '0.3rem',
|
||||||
|
color: theme.palette.grey[700],
|
||||||
|
}));
|
||||||
|
|
||||||
interface IGroupForm {
|
interface IGroupForm {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -76,7 +95,14 @@ export const GroupForm: FC<IGroupForm> = ({
|
|||||||
mode,
|
mode,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig, isOss } = useUiConfig();
|
||||||
|
|
||||||
|
const { config: oidcSettings } = useAuthSettings('oidc');
|
||||||
|
const { config: samlSettings } = useAuthSettings('saml');
|
||||||
|
|
||||||
|
const isGroupSyncingEnabled =
|
||||||
|
(oidcSettings?.enabled && oidcSettings.enableGroupSyncing) ||
|
||||||
|
(samlSettings?.enabled && samlSettings.enableGroupSyncing);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledForm onSubmit={handleSubmit}>
|
<StyledForm onSubmit={handleSubmit}>
|
||||||
@ -108,18 +134,46 @@ export const GroupForm: FC<IGroupForm> = ({
|
|||||||
data-testid={UG_DESC_ID}
|
data-testid={UG_DESC_ID}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(uiConfig.flags.syncSSOGroups)}
|
condition={
|
||||||
|
Boolean(uiConfig.flags.syncSSOGroups) && !isOss()
|
||||||
|
}
|
||||||
show={
|
show={
|
||||||
<>
|
<ConditionallyRender
|
||||||
<StyledInputDescription>
|
condition={isGroupSyncingEnabled}
|
||||||
Is this group associated with SSO groups?
|
show={
|
||||||
</StyledInputDescription>
|
<>
|
||||||
<StyledItemList
|
<StyledInputDescription>
|
||||||
label="SSO group ID / name"
|
Is this group associated with SSO
|
||||||
value={mappingsSSO}
|
groups?
|
||||||
onChange={setMappingsSSO}
|
</StyledInputDescription>
|
||||||
/>
|
<StyledItemList
|
||||||
</>
|
label="SSO group ID / name"
|
||||||
|
value={mappingsSSO}
|
||||||
|
onChange={setMappingsSSO}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={() => (
|
||||||
|
<StyledDescriptionBlock>
|
||||||
|
<div>
|
||||||
|
You can enable SSO groups syncronization
|
||||||
|
if needed
|
||||||
|
<Tooltip
|
||||||
|
title="You can enable SSO groups
|
||||||
|
syncronization if needed"
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<StyledHelpOutline />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<Link data-loading to={`/admin/auth`}>
|
||||||
|
<span data-loading>
|
||||||
|
View SSO configuration
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</StyledDescriptionBlock>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -15,6 +15,7 @@ export const mapGroupUsers = (users: any[]) =>
|
|||||||
users.map(user => ({
|
users.map(user => ({
|
||||||
...user.user,
|
...user.user,
|
||||||
joinedAt: new Date(user.joinedAt),
|
joinedAt: new Date(user.joinedAt),
|
||||||
|
createdBy: user.createdBy,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useGroup = (groupId: number): IUseGroupOutput => {
|
export const useGroup = (groupId: number): IUseGroupOutput => {
|
||||||
|
@ -51,6 +51,7 @@ const rowToGroupUser = (row) => {
|
|||||||
userId: row.user_id,
|
userId: row.user_id,
|
||||||
groupId: row.group_id,
|
groupId: row.group_id,
|
||||||
joinedAt: row.created_at,
|
joinedAt: row.created_at,
|
||||||
|
createdBy: row.created_by,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -117,7 +118,12 @@ export default class GroupStore implements IGroupStore {
|
|||||||
|
|
||||||
async getAllUsersByGroups(groupIds: number[]): Promise<IGroupUser[]> {
|
async getAllUsersByGroups(groupIds: number[]): Promise<IGroupUser[]> {
|
||||||
const rows = await this.db
|
const rows = await this.db
|
||||||
.select('gu.group_id', 'u.id as user_id', 'gu.created_at')
|
.select(
|
||||||
|
'gu.group_id',
|
||||||
|
'u.id as user_id',
|
||||||
|
'gu.created_at',
|
||||||
|
'gu.created_by',
|
||||||
|
)
|
||||||
.from(`${T.GROUP_USER} AS gu`)
|
.from(`${T.GROUP_USER} AS gu`)
|
||||||
.join(`${T.USERS} AS u`, 'u.id', 'gu.user_id')
|
.join(`${T.USERS} AS u`, 'u.id', 'gu.user_id')
|
||||||
.whereIn('gu.group_id', groupIds);
|
.whereIn('gu.group_id', groupIds);
|
||||||
|
@ -11,6 +11,10 @@ export const groupUserModelSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
|
createdBy: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
user: {
|
user: {
|
||||||
$ref: '#/components/schemas/userSchema',
|
$ref: '#/components/schemas/userSchema',
|
||||||
},
|
},
|
||||||
|
@ -211,6 +211,7 @@ export class GroupService {
|
|||||||
return {
|
return {
|
||||||
user: user,
|
user: user,
|
||||||
joinedAt: roleUser.joinedAt,
|
joinedAt: roleUser.joinedAt,
|
||||||
|
createdBy: roleUser.createdBy,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return { ...group, users: finalUsers };
|
return { ...group, users: finalUsers };
|
||||||
@ -219,6 +220,7 @@ export class GroupService {
|
|||||||
async syncExternalGroups(
|
async syncExternalGroups(
|
||||||
userId: number,
|
userId: number,
|
||||||
externalGroups: string[],
|
externalGroups: string[],
|
||||||
|
createdBy?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (Array.isArray(externalGroups)) {
|
if (Array.isArray(externalGroups)) {
|
||||||
let newGroups = await this.groupStore.getNewGroupsForExternalUser(
|
let newGroups = await this.groupStore.getNewGroupsForExternalUser(
|
||||||
@ -228,6 +230,7 @@ export class GroupService {
|
|||||||
await this.groupStore.addUserToGroups(
|
await this.groupStore.addUserToGroups(
|
||||||
userId,
|
userId,
|
||||||
newGroups.map((g) => g.id),
|
newGroups.map((g) => g.id),
|
||||||
|
createdBy,
|
||||||
);
|
);
|
||||||
let oldGroups = await this.groupStore.getOldGroupsForExternalUser(
|
let oldGroups = await this.groupStore.getOldGroupsForExternalUser(
|
||||||
userId,
|
userId,
|
||||||
|
@ -16,6 +16,7 @@ export interface IGroupUser {
|
|||||||
userId: number;
|
userId: number;
|
||||||
joinedAt: Date;
|
joinedAt: Date;
|
||||||
seenAt?: Date;
|
seenAt?: Date;
|
||||||
|
createdBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGroupRole {
|
export interface IGroupRole {
|
||||||
@ -38,6 +39,7 @@ export interface IGroupProject {
|
|||||||
export interface IGroupUserModel {
|
export interface IGroupUserModel {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
joinedAt?: Date;
|
joinedAt?: Date;
|
||||||
|
createdBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGroupModelWithProjectRole extends IGroupModel {
|
export interface IGroupModelWithProjectRole extends IGroupModel {
|
||||||
|
@ -1420,6 +1420,10 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
"groupUserModelSchema": {
|
"groupUserModelSchema": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"createdBy": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
"joinedAt": {
|
"joinedAt": {
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
Loading…
Reference in New Issue
Block a user