diff --git a/frontend/src/component/admin/groups/Group/Group.tsx b/frontend/src/component/admin/groups/Group/Group.tsx index a38360cd51..907990755a 100644 --- a/frontend/src/component/admin/groups/Group/Group.tsx +++ b/frontend/src/component/admin/groups/Group/Group.tsx @@ -111,6 +111,14 @@ export const Group: VFC = () => { sortType: 'date', maxWidth: 150, }, + { + id: 'createdBy', + Header: 'Added by', + accessor: 'createdBy', + Cell: HighlightCell, + minWidth: 90, + searchable: true, + }, { Header: 'Last login', accessor: (row: IGroupUser) => row.seenAt || '', diff --git a/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx b/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx index 1b04ff4247..9108797fd5 100644 --- a/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx +++ b/frontend/src/component/admin/groups/GroupForm/GroupForm.tsx @@ -1,5 +1,5 @@ -import { FC } from 'react'; -import { Button, styled } from '@mui/material'; +import React, { FC } from 'react'; +import { Button, styled, Tooltip } from '@mui/material'; import { UG_DESC_ID, UG_NAME_ID } from 'utils/testIds'; import Input from 'component/common/Input/Input'; import { IGroupUser } from 'interfaces/group'; @@ -8,6 +8,9 @@ import { GroupFormUsersSelect } from './GroupFormUsersSelect/GroupFormUsersSelec import { GroupFormUsersTable } from './GroupFormUsersTable/GroupFormUsersTable'; import { ItemList } from 'component/common/ItemList/ItemList'; 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')(() => ({ display: 'flex', @@ -22,13 +25,13 @@ const StyledInputDescription = styled('p')(({ theme }) => ({ const StyledInput = styled(Input)(({ theme }) => ({ width: '100%', - maxWidth: theme.spacing(50), + maxWidth: theme.spacing(60), marginBottom: theme.spacing(2), })); const StyledItemList = styled(ItemList)(({ theme }) => ({ width: '100%', - maxWidth: theme.spacing(50), + maxWidth: theme.spacing(60), marginBottom: theme.spacing(2), })); @@ -46,6 +49,22 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({ 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 { name: string; description: string; @@ -76,7 +95,14 @@ export const GroupForm: FC = ({ mode, 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 ( @@ -108,18 +134,46 @@ export const GroupForm: FC = ({ data-testid={UG_DESC_ID} /> - - Is this group associated with SSO groups? - - - + + + Is this group associated with SSO + groups? + + + + } + elseShow={() => ( + +
+ You can enable SSO groups syncronization + if needed + + + +
+ + + View SSO configuration + + +
+ )} + /> } /> users.map(user => ({ ...user.user, joinedAt: new Date(user.joinedAt), + createdBy: user.createdBy, })); export const useGroup = (groupId: number): IUseGroupOutput => { diff --git a/src/lib/db/group-store.ts b/src/lib/db/group-store.ts index 63315b7b5f..b9f3489d2e 100644 --- a/src/lib/db/group-store.ts +++ b/src/lib/db/group-store.ts @@ -51,6 +51,7 @@ const rowToGroupUser = (row) => { userId: row.user_id, groupId: row.group_id, joinedAt: row.created_at, + createdBy: row.created_by, }; }; @@ -117,7 +118,12 @@ export default class GroupStore implements IGroupStore { async getAllUsersByGroups(groupIds: number[]): Promise { 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`) .join(`${T.USERS} AS u`, 'u.id', 'gu.user_id') .whereIn('gu.group_id', groupIds); diff --git a/src/lib/openapi/spec/group-user-model-schema.ts b/src/lib/openapi/spec/group-user-model-schema.ts index 6f287ac73e..765de66c30 100644 --- a/src/lib/openapi/spec/group-user-model-schema.ts +++ b/src/lib/openapi/spec/group-user-model-schema.ts @@ -11,6 +11,10 @@ export const groupUserModelSchema = { type: 'string', format: 'date-time', }, + createdBy: { + type: 'string', + nullable: true, + }, user: { $ref: '#/components/schemas/userSchema', }, diff --git a/src/lib/services/group-service.ts b/src/lib/services/group-service.ts index 46c5995afe..2ad66a7487 100644 --- a/src/lib/services/group-service.ts +++ b/src/lib/services/group-service.ts @@ -211,6 +211,7 @@ export class GroupService { return { user: user, joinedAt: roleUser.joinedAt, + createdBy: roleUser.createdBy, }; }); return { ...group, users: finalUsers }; @@ -219,6 +220,7 @@ export class GroupService { async syncExternalGroups( userId: number, externalGroups: string[], + createdBy?: string, ): Promise { if (Array.isArray(externalGroups)) { let newGroups = await this.groupStore.getNewGroupsForExternalUser( @@ -228,6 +230,7 @@ export class GroupService { await this.groupStore.addUserToGroups( userId, newGroups.map((g) => g.id), + createdBy, ); let oldGroups = await this.groupStore.getOldGroupsForExternalUser( userId, diff --git a/src/lib/types/group.ts b/src/lib/types/group.ts index 4c83a774d6..e987334372 100644 --- a/src/lib/types/group.ts +++ b/src/lib/types/group.ts @@ -16,6 +16,7 @@ export interface IGroupUser { userId: number; joinedAt: Date; seenAt?: Date; + createdBy?: string; } export interface IGroupRole { @@ -38,6 +39,7 @@ export interface IGroupProject { export interface IGroupUserModel { user: IUser; joinedAt?: Date; + createdBy?: string; } export interface IGroupModelWithProjectRole extends IGroupModel { 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 35dace52bd..1ed06f43d7 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 @@ -1420,6 +1420,10 @@ exports[`should serve the OpenAPI spec 1`] = ` "groupUserModelSchema": { "additionalProperties": false, "properties": { + "createdBy": { + "nullable": true, + "type": "string", + }, "joinedAt": { "format": "date-time", "type": "string",