From 7058d6b98e7996a1c29842fae3d4eeb85f873447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mai=20G=C3=A1bor?= Date: Mon, 18 Nov 2024 09:32:44 +0100 Subject: [PATCH] Feat mgmt client (#205) * Start moving mgmt-client into client * Add tenant OAuth + FQDN mgmt components * Add room,group,role,user tables.Add local admin login+bugix for login state * add management view * Update sonarqube.yml * Update sonarqube.yml * Update sonarqube.yml import ImpressumButton from '../../components/controlbuttons/ImpressumButton'; * Update sonarqube.yml * Update sonarqube.yml * Update sonarqube.yml * Update sonarqube.yml * Update sonarqube.yml * Update sonarqube.yml * Update sonarqube.yml * Update sonarqube.yml * Add sonar project key * move management up to router level * add permissions table * add Tenant Owner+ tenant Admin table * fix mgmt-admin login for tentants. * add room owners * Add group role * add group user * add room user role table * add management current room settings * add functionality to claim room. * add notifications to MGMT and remove console.log statements * remove redundant react imports * add titles to mgmt-admin components * hide mgmt UI if login is not enabled * add success messages * fix typo, and remove unused import --- .github/workflows/sonarqube.yml | 15 +- package.json | 11 +- sonar-project.properties | 1 + .../managementservice/groups/GroupRole.tsx | 398 +++++++++++ .../managementservice/groups/GroupUser.tsx | 316 +++++++++ .../managementservice/groups/Groups.tsx | 300 ++++++++ .../permisssion/Permission.tsx | 206 ++++++ .../managementservice/role/Role.tsx | 385 ++++++++++ .../managementservice/rooms/CurrentRoom.tsx | 376 ++++++++++ .../managementservice/rooms/Room.tsx | 657 ++++++++++++++++++ .../managementservice/rooms/RoomOwner.tsx | 318 +++++++++ .../managementservice/rooms/roomUserRole.tsx | 431 ++++++++++++ .../managementservice/tenants/Tenant.tsx | 196 ++++++ .../managementservice/tenants/TenantAdmin.tsx | 308 ++++++++ .../managementservice/tenants/TenantOAuth.tsx | 470 +++++++++++++ .../managementservice/tenants/TenantOwner.tsx | 313 +++++++++ .../managementservice/tenants/TenatnFQDN.tsx | 252 +++++++ .../managementservice/users/Users.tsx | 368 ++++++++++ src/components/menuitems/Login.tsx | 14 +- src/components/precalltitle/PrecallTitle.tsx | 17 +- .../settingsdialog/ManagementSettings.tsx | 59 ++ .../settingsdialog/SettingsDialog.tsx | 17 +- .../ManagementAdminLoginSettings.tsx | 83 +++ .../translated/translatedComponents.tsx | 33 + src/index.tsx | 8 + src/store/actions/managementActions.tsx | 222 +++++- src/store/actions/mgmtActions.tsx | 39 ++ src/store/actions/permissionsActions.tsx | 59 ++ src/store/slices/roomSlice.tsx | 2 +- src/store/slices/uiSlice.tsx | 2 +- src/utils/types.tsx | 125 ++++ src/views/join/Join.tsx | 2 - src/views/management/Management.tsx | 316 +++++++++ yarn.lock | 227 ++++-- 34 files changed, 6452 insertions(+), 94 deletions(-) create mode 100644 sonar-project.properties create mode 100644 src/components/managementservice/groups/GroupRole.tsx create mode 100644 src/components/managementservice/groups/GroupUser.tsx create mode 100644 src/components/managementservice/groups/Groups.tsx create mode 100644 src/components/managementservice/permisssion/Permission.tsx create mode 100644 src/components/managementservice/role/Role.tsx create mode 100644 src/components/managementservice/rooms/CurrentRoom.tsx create mode 100644 src/components/managementservice/rooms/Room.tsx create mode 100644 src/components/managementservice/rooms/RoomOwner.tsx create mode 100644 src/components/managementservice/rooms/roomUserRole.tsx create mode 100644 src/components/managementservice/tenants/Tenant.tsx create mode 100644 src/components/managementservice/tenants/TenantAdmin.tsx create mode 100644 src/components/managementservice/tenants/TenantOAuth.tsx create mode 100644 src/components/managementservice/tenants/TenantOwner.tsx create mode 100644 src/components/managementservice/tenants/TenatnFQDN.tsx create mode 100644 src/components/managementservice/users/Users.tsx create mode 100644 src/components/settingsdialog/ManagementSettings.tsx create mode 100644 src/components/settingsdialog/managementsettings/ManagementAdminLoginSettings.tsx create mode 100644 src/store/actions/mgmtActions.tsx create mode 100644 src/views/management/Management.tsx diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index eb7d21fc..709db523 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,15 +1,14 @@ name: SonarQube CI on: push jobs: - sonarQubeTrigger: - name: SonarQube Trigger + sonarqube: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: SonarQube Scan - uses: kitabisa/sonarqube-action@v1.2.1 - with: - host: ${{ secrets.SONARQUBE_HOST }} - login: ${{ secrets.SONARQUBE_TOKEN }} + - name: SonarQube Scaner + uses: sonarsource/sonarqube-scan-action@v3.0.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} diff --git a/package.json b/package.json index 37ca89ab..ca6a9407 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,10 @@ "@feathersjs/authentication-client": "^5.0.30", "@feathersjs/feathers": "^5.0.30", "@feathersjs/rest-client": "^5.0.30", - "@mui/icons-material": "^5.16.7", - "@mui/material": "^5.16.7", + "@feathersjs/socketio-client": "^5.0.30", + "@mui/icons-material": "^6.1.5", + "@mui/material": "^6.1.5", + "@mui/x-date-pickers": "^7.22.0", "@observertc/client-monitor-js": "^3.11.0", "@observertc/samples-encoder": "^2.2.12", "@reduxjs/toolkit": "^1.9.7", @@ -25,6 +27,7 @@ "fscreen": "^1.2.0", "hark": "^1.2.3", "marked": "^9.1.6", + "material-react-table": "^3.0.1", "mediasoup-client": "^3.7.16", "native-file-system-adapter": "^3.0.1", "notistack": "^3.0.1", @@ -71,8 +74,8 @@ "@types/hark": "^1.2.5", "@types/node": "^20.16.5", "@types/random-string": "^0.0.30", - "@types/react": "^18.3.8", - "@types/react-dom": "^18.3.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "@types/react-redux": "^7.1.33", "@types/redux-logger": "^3.0.13", "@types/sdp-transform": "^2.4.9", diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..ec8b390b --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1 @@ +sonar.projectKey=edumeet-client diff --git a/src/components/managementservice/groups/GroupRole.tsx b/src/components/managementservice/groups/GroupRole.tsx new file mode 100644 index 00000000..b3ece23b --- /dev/null +++ b/src/components/managementservice/groups/GroupRole.tsx @@ -0,0 +1,398 @@ +/* eslint-disable camelcase */ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Autocomplete } from '@mui/material'; +import { GroupRoles, Groups, Roles, Room } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +const GroupRoleTable = () => { + const dispatch = useAppDispatch(); + + type RoomOptionTypes = Array + + // nested data is ok, see accessorKeys in ColumnDef below + const [ rooms, setRooms ] = useState([ { + 'id': 1, + 'name': '', + 'description': '', + 'createdAt': '', + 'updatedAt': '', + 'creatorId': '', + 'defaultRoleId': '', + 'tenantId': 1, + 'logo': null, + 'background': null, + 'maxActiveVideos': 0, + 'locked': true, + 'chatEnabled': true, + 'raiseHandEnabled': true, + 'filesharingEnabled': true, + 'groupRoles': [], + 'localRecordingEnabled': true, + 'owners': [], + 'breakoutsEnabled': true, + } + ]); + + type RolesOptionTypes = Array + + // nested data is ok, see accessorKeys in ColumnDef below + const [ roles, setRoles ] = useState([ { + 'id': 0, + 'name': '', + 'description': '', + 'tenantId': 0, + 'permissions': [] + } + ]); + + const getRoleName = (id: string): string => { + const t = roles.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined role'; + } + }; + + type GroupsOptionTypes = Array + + // nested data is ok, see accessorKeys in ColumnDef below + const [ groups, setGroups ] = useState([ { + 'id': 0, + 'name': '', + 'description': '', + 'tenantId': 0 + } + ]); + + const getGroupsName = (id: string): string => { + const t = groups.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined group'; + } + }; + + const getRoomName = (id: string): string => { + const t = rooms.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined room'; + } + }; + + // should be memoized or stable + const columns = useMemo[]>( + () => [ + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'groupId', + header: 'Group', + Cell: ({ cell }) => getGroupsName(cell.getValue()) + + }, + { + accessorKey: 'roleId', + header: 'Role', + Cell: ({ cell }) => getRoleName(cell.getValue()) + + }, + { + accessorKey: 'roomId', + header: 'Room', + Cell: ({ cell }) => getRoomName(cell.getValue()) + + }, + + /* { + accessorKey: 'role', + header: 'role', + Cell: ({ cell }) => + ( + cell.getValue().name + ), + }, */ + ], + [ rooms, roles, groups ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ groupId, setGroupId ] = useState(0); + const [ roleId, setRoleId ] = useState(0); + const [ roomId, setRoomId ] = useState(0); + + const [ groupIdOption, setGroupIdOption ] = useState(); + const [ roleIdOption, setRoleIdOption ] = useState(); + const [ roomIdOption, setRoomIdOption ] = useState(); + const [ groupIdOptionDisabled, setGroupIdOptionDisabled ] = useState(true); + const [ roleIdOptionDisabled, setRoleIdOptionDisabled ] = useState(true); + const [ roomIdOptionDisabled, setRoomIdOptionDisabled ] = useState(true); + + const [ cantPatch, setCantPatch ] = useState(true); + const [ cantDelete ] = useState(false); + + async function fetchProduct() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('groups')).then((tdata: any) => { + if (tdata != undefined) { + setGroups(tdata.data); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('rooms')).then((tdata: any) => { + if (tdata != undefined) { + setRooms(tdata.data); + } + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('roles')).then((tdata: any) => { + if (tdata != undefined) { + setRoles(tdata.data); + } + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('roomGroupRoles')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata); + } + setIsLoading(false); + + }); + + } + + useEffect(() => { + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleClickOpen = () => { + setId(0); + setGroupId(0); + setRoleId(0); + setRoomId(0); + setGroupIdOption(undefined); + setRoleIdOption(undefined); + setRoomIdOption(undefined); + setGroupIdOptionDisabled(false); + setRoleIdOptionDisabled(false); + setRoomIdOptionDisabled(false); + setCantPatch(false); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setGroupIdOptionDisabled(true); + setRoleIdOptionDisabled(true); + setRoomIdOptionDisabled(true); + setCantPatch(true); + setOpen(true); + }; + + const handleGroupIdChange = (event: SyntheticEvent, newValue: Groups) => { + if (newValue) { + setGroupId(newValue.id); + setGroupIdOption(newValue); + } + }; + const handleRoleIdChange = (event: SyntheticEvent, newValue: Roles) => { + if (newValue) { + setRoleId(newValue.id); + setRoleIdOption(newValue); + } + }; + const handleRoomIdChange = (event: SyntheticEvent, newValue: Room) => { + if (newValue && typeof newValue.id === 'number') { + setRoomId(newValue.id); + setRoomIdOption(newValue); + } + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + dispatch(deleteData(id, 'roomGroupRoles')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (id === 0) { + dispatch(createData({ + groupId: groupId, + roleId: roleId, + roomId: roomId + }, 'roomGroupRoles')).then(() => { + fetchProduct(); + setOpen(false); + }); + + } else if (id != 0) { + dispatch(patchData(id, { + groupId: groupId, + roleId: roleId, + roomId: roomId + }, 'roomGroupRoles')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + option.name} + fullWidth + disableClearable + readOnly={groupIdOptionDisabled} + onChange={handleGroupIdChange} + value={groupIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + option.name} + fullWidth + disableClearable + readOnly={roleIdOptionDisabled} + onChange={handleRoleIdChange} + value={roleIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + ((typeof option.name == 'string')?option.name:'')} + fullWidth + disableClearable + readOnly={roomIdOptionDisabled} + onChange={handleRoomIdChange} + value={roomIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const tgroupId=r[1].getValue(); + const troleId=r[2].getValue(); + const troomId=r[3].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof tgroupId === 'string') { + setGroupId(parseInt(tgroupId)); + } else { + setGroupId(0); + } + + if (typeof tgroupId === 'string') { + const tgroup = groups.find((x) => x.id === parseInt(tgroupId)); + + if (tgroup) { + setGroupIdOption(tgroup); + } + setGroupId(parseInt(tgroupId)); + } else { + setGroupId(0); + setGroupIdOption(undefined); + } + + if (typeof troleId === 'string') { + const troles = roles.find((x) => x.id === parseInt(troleId)); + + if (troles) { + setRoleIdOption(troles); + } + setRoleId(parseInt(troleId)); + } else { + setRoleId(0); + setRoleIdOption(undefined); + } + if (typeof troomId === 'string') { + const troom = rooms.find((x) => x.id === parseInt(troomId)); + + if (troom) { + setRoomIdOption(troom); + } + setRoomId(parseInt(troomId)); + } else { + setRoomId(0); + setRoomIdOption(undefined); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + } + }} + state={{ isLoading }} + /> + ; +}; + +export default GroupRoleTable; diff --git a/src/components/managementservice/groups/GroupUser.tsx b/src/components/managementservice/groups/GroupUser.tsx new file mode 100644 index 00000000..c58f7fea --- /dev/null +++ b/src/components/managementservice/groups/GroupUser.tsx @@ -0,0 +1,316 @@ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Autocomplete } from '@mui/material'; +import { Groups, GroupUsers, User } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +const GroupUserTable = () => { + const dispatch = useAppDispatch(); + + type GroupsTypes = Array + type UserTypes = Array + + const [ groups, setGroups ] = useState([ { + id: 0, + name: 'string', + description: 'string', + tenantId: 0 + } ]); + + const [ users, setUsers ] = useState([ { + 'id': 0, + 'ssoId': '', + 'tenantId': 0, + 'email': '', + 'name': '', + 'avatar': '', + 'roles': [], + 'tenantAdmin': false, + 'tenantOwner': false + } ]); + + const getGroupName = (id: string): string => { + const t = groups.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined group'; + } + }; + const getUserEmail = (id: string): string => { + const t = users.find((type) => type.id === parseInt(id)); + + if (t && t.email) { + return t.email; + } else { + return 'no such email'; + } + }; + + // should be memoized or stable + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'groupId', + header: 'Group', + Cell: ({ cell }) => getGroupName(cell.getValue()) + }, + { + accessorKey: 'userId', + header: 'User', + Cell: ({ cell }) => getUserEmail(cell.getValue()) + + } + ], + [ groups, users ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ groupId, setGroupId ] = useState(0); + const [ cantPatch, setCantPatch ] = useState(false); + const [ cantDelete ] = useState(false); + const [ userId, setUserId ] = useState(0); + const [ groupIdDisabled, setGroupIdDisabled ] = useState(false); + const [ userIdDisabled, setUserIdDisabled ] = useState(false); + + const [ groupIdOption, setGroupIdOption ] = useState(); + const [ userIdOption, setUserIdOption ] = useState(); + + async function fetchProduct() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('groups')).then((tdata: any) => { + if (tdata != undefined) { + setGroups(tdata.data); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('users')).then((tdata: any) => { + if (tdata != undefined) { + setUsers(tdata.data); + } + + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('groupUsers')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata); + } + setIsLoading(false); + + }); + + } + + useEffect(() => { + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleClickOpen = () => { + setId(0); + setGroupId(0); + setGroupIdDisabled(false); + setUserId(0); + setUserIdDisabled(false); + setUserIdOption(undefined); + setGroupIdOption(undefined); + setCantPatch(false); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setGroupIdDisabled(true); + + setUserIdDisabled(true); + + setCantPatch(true); + setOpen(true); + }; + + const handleGroupIdChange = (event: SyntheticEvent, newValue: Groups) => { + if (newValue) { + setGroupId(newValue.id); + setGroupIdOption(newValue); + } + }; + + const handleUserIdChange = (event: SyntheticEvent, newValue: User) => { + if (newValue) { + setUserId(newValue.id); + setUserIdOption(newValue); + } + }; + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + dispatch(deleteData(id, 'groupUsers')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (id === 0) { + dispatch(createData({ + groupId: groupId, + userId: userId + }, 'groupUsers')).then(() => { + fetchProduct(); + setOpen(false); + }); + + } else if (id != 0) { + dispatch(patchData(id, { + groupId: groupId, + userId: userId + }, 'groupUsers')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + {/* */} + option.name} + fullWidth + disableClearable + readOnly={groupIdDisabled} + onChange={handleGroupIdChange} + value={groupIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + {/* */} + option.email} + fullWidth + disableClearable + readOnly={userIdDisabled} + onChange={handleUserIdChange} + value={userIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const tgroupId=r[1].getValue(); + const tuserId=r[2].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + + if (typeof tgroupId === 'string') { + const tgroup = groups.find((x) => x.id === parseInt(tgroupId)); + + if (tgroup) { + setGroupIdOption(tgroup); + } + setGroupId(parseInt(tgroupId)); + } else { + setGroupId(0); + } + + if (typeof tuserId === 'string') { + const tuser = users.find((x) => x.id === parseInt(tuserId)); + + if (tuser) { + setUserIdOption(tuser); + } + setUserId(parseInt(tuserId)); + } else { + setUserId(0); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + } + }} + state={{ isLoading }} + /> + ; +}; + +export default GroupUserTable; diff --git a/src/components/managementservice/groups/Groups.tsx b/src/components/managementservice/groups/Groups.tsx new file mode 100644 index 00000000..2bfd3425 --- /dev/null +++ b/src/components/managementservice/groups/Groups.tsx @@ -0,0 +1,300 @@ +import Autocomplete from '@mui/material/Autocomplete'; +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions } from '@mui/material'; +import React from 'react'; +import { Groups, Tenant } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; +import { notificationsActions } from '../../../store/slices/notificationsSlice'; + +const GroupTable = () => { + const dispatch = useAppDispatch(); + + type TenantOptionTypes = Array + + const [ tenants, setTenants ] = useState([ { 'id': 0, 'name': '', 'description': '' } ]); + + const getTenantName = (id: string): string => { + const t = tenants.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined tenant'; + } + }; + + // should be memoized or stable + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'name', + header: 'Name' + }, + { + accessorKey: 'description', + header: 'description' + }, + { + accessorKey: 'tenantId', + header: 'Tenant', + Cell: ({ cell }) => getTenantName(cell.getValue()) + } + ], + [ tenants ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ name, setName ] = useState(''); + const [ description, setDescription ] = useState(''); + const [ cantPatch ] = useState(false); + const [ cantDelete ] = useState(false); + const [ tenantId, setTenantId ] = useState(0); + + const [ tenantIdOption, setTenantIdOption ] = useState(); + const [ descriptionDisabled, setDescriptionDisabled ] = useState(false); + const [ tenantIdDisabled, setTenantIdDisabled ] = useState(false); + + async function fetchProduct() { + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenants')).then((tdata: any) => { + if (tdata != undefined) { + setTenants(tdata.data); + } + setIsLoading(false); + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('groups')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + setIsLoading(false); + + }); + + } + + useEffect(() => { + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = React.useState(false); + + // ADD NEW + const handleOpen = () => { + setId(0); + setName(''); + setDescription(''); + setDescriptionDisabled(false); + // try to get current tenantId + // TODO + setTenantId(0); + setTenantIdOption(undefined); + + setTenantIdDisabled(true); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setDescriptionDisabled(false); + setTenantIdDisabled(false); + // get tenantId from clicked element + setOpen(true); + }; + + const handleNameChange = (event: { target: { value: React.SetStateAction; }; }) => { + setName(event.target.value); + }; + const handleDescriptionChange = (event: { target: { value: React.SetStateAction; }; }) => { + setDescription(event.target.value); + }; + + const handleTenantIdChange = (event: SyntheticEvent, newValue: Tenant) => { + if (newValue) { + setTenantId(newValue.id); + setTenantIdOption(newValue); + } + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'groups')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (name != '' && id === 0) { + dispatch(createData({ + name: name, + }, 'groups')).then(() => { + fetchProduct(); + setOpen(false); + }); + } else if (name != '' && id != 0) { + dispatch(patchData(id, + { + name: name, + description: description, + tenantId: tenantId + }, 'groups')).then(() => { + fetchProduct(); + setOpen(false); + }); + } else { + dispatch(notificationsActions.enqueueNotification({ + message: 'Name cannot be empty!', + options: { variant: 'warning' } + })); + } + + }; + + return <> +
+ + +
+ + + Add/Edit + + + These are the parameters that you can change. + + + + + {/* */} + option.name} + fullWidth + disableClearable + id="combo-box-demo" + readOnly={tenantIdDisabled} + onChange={handleTenantIdChange} + value={tenantIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const tname = r[1].getValue(); + const tdescription = r[2].getValue(); + const ttenantId = r[3].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof tname === 'string') { + setName(tname); + } else { + setName(''); + } + if (typeof tdescription === 'string') { + setDescription(tdescription); + } else { + setDescription(''); + } + if (typeof ttenantId === 'string') { + const ttenant = tenants.find((x) => x.id === parseInt(ttenantId)); + + if (ttenant) { + setTenantIdOption(ttenant); + } + setTenantId(parseInt(ttenantId)); + } else { + setTenantId(0); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + } + }} + state={{ isLoading }} + /> + ; +}; + +export default GroupTable; diff --git a/src/components/managementservice/permisssion/Permission.tsx b/src/components/managementservice/permisssion/Permission.tsx new file mode 100644 index 00000000..b3f4f1d2 --- /dev/null +++ b/src/components/managementservice/permisssion/Permission.tsx @@ -0,0 +1,206 @@ +import { useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions } from '@mui/material'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; +import { Permissions } from '../../../utils/types'; + +const PermissionTable = () => { + const dispatch = useAppDispatch(); + + // should be memoized or stable + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'name', + header: 'Name' + }, + { + accessorKey: 'description', + header: 'description' + }, + + ], + [], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ name, setName ] = useState(''); + const [ description, setDescription ] = useState(''); + const [ cantPatch ] = useState(true); + const [ cantDelete ] = useState(true); + + async function fetchProduct() { + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('permissions')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + setIsLoading(false); + + }); + + } + + useEffect(() => { + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleClickOpen = () => { + setId(0); + setName(''); + setDescription(''); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setOpen(true); + }; + + const handleNameChange = (event: { target: { value: React.SetStateAction; }; }) => { + setName(event.target.value); + }; + const handleDescriptionChange = (event: { target: { value: React.SetStateAction; }; }) => { + setDescription(event.target.value); + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'permissions')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (name != '' && id === 0) { + dispatch(createData({ + name: name, + description: description + }, 'permissions')).then(() => { + fetchProduct(); + setOpen(false); + }); + + } else if (name != '' && id != 0) { + dispatch(patchData(id, { + name: name, + description: description + }, 'permissions')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + + Add/Edit + + + These are the parameters that you can change. + + + + + + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const tname=r[1].getValue(); + const tdescription=r[2].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof tname === 'string') { + setName(tname); + } else { + setName(''); + } + if (typeof tdescription === 'string') { + setDescription(tdescription); + } else { + setDescription(''); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + } + }} + state={{ isLoading }} + /> + ; +}; + +export default PermissionTable; diff --git a/src/components/managementservice/role/Role.tsx b/src/components/managementservice/role/Role.tsx new file mode 100644 index 00000000..31cea6b2 --- /dev/null +++ b/src/components/managementservice/role/Role.tsx @@ -0,0 +1,385 @@ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Autocomplete, FormControlLabel, Checkbox, Box } from '@mui/material'; +import React from 'react'; +import { Roles, Tenant, Permissions, RolePermissions } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +const RoleTable = () => { + const dispatch = useAppDispatch(); + + type TenantOptionTypes = Array + + const [ tenants, setTenants ] = useState([ { 'id': 0, 'name': '', 'description': '' } ]); + + const getTenantName = (id: string): string => { + const t = tenants.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined tenant'; + } + }; + // should be memoized or stable + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'name', + header: 'Name' + }, + { + accessorKey: 'description', + header: 'description' + }, + { + accessorKey: 'tenantId', + header: 'Tenant', + Cell: ({ cell }) => getTenantName(cell.getValue()) + + }, + { + accessorKey: 'permissions', + header: 'Permission(s)', + Cell: ({ cell }) => + ( + cell.getValue>().map((single: Permissions) => single.name) + .join(', ') + ), + }, + + ], + [ tenants ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ name, setName ] = useState(''); + const [ description, setDescription ] = useState(''); + const [ tenantId, setTenantId ] = useState(0); + + const [ cantPatch ] = useState(false); + const [ cantDelete ] = useState(false); + const [ tenantIdOption, setTenantIdOption ] = useState(); + + async function fetchProduct() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenants')).then((tdata: any) => { + if (tdata != undefined) { + setTenants(tdata.data); + } + setIsLoading(false); + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('roles')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + setIsLoading(false); + + }); + } + + useEffect(() => { + + async function getPermissions() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('permissions')).then((tdata: any) => { + if (tdata != undefined) { + setPermissions(tdata.data); + setChecked(new Array(tdata.data.length).fill(true)); + + } + setIsLoading(false); + + }); + + } + getPermissions(); + + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = React.useState(false); + + const handleClickOpen = () => { + setId(0); + setName(''); + setDescription(''); + setTenantId(0); + setChecked(new Array(permissions.length).fill(false)); + + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setOpen(true); + }; + + const handleNameChange = (event: { target: { value: React.SetStateAction; }; }) => { + setName(event.target.value); + }; + const handleDescriptionChange = (event: { target: { value: React.SetStateAction; }; }) => { + setDescription(event.target.value); + }; + const handleTenantIdChange = (event: SyntheticEvent, newValue: Tenant) => { + if (newValue) { + setTenantId(newValue.id); + setTenantIdOption(newValue); + } + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'roles')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (name != '' && id === 0) { + dispatch(createData({ + name: name, + description: description, + tenantId: tenantId + }, 'roles')).then(() => { + fetchProduct(); + setOpen(false); + + }); + } else if (name != '' && id != 0) { + + dispatch(patchData(id, { + name: name, + description: description, + tenantId: tenantId + }, 'roles')).then(() => { + fetchProduct(); + setOpen(false); + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('rolePermissions')).then((rp: any) => { + + checked.forEach(async (element, index) => { + const c = rp.data.filter((x: RolePermissions) => x.permissionId == index+1); + + if ((c.length === 0) === element) { + + if (element) { + + dispatch(createData({ + roleId: id, + permissionId: index+1 + }, 'rolePermissions')).then(() => { + fetchProduct(); + setOpen(false); + }); + } else { + // remove role + dispatch(deleteData(c[0].id, 'rolePermissions')).then(() => { + fetchProduct(); + setOpen(false); + }); + + } + + } + + }); + fetchProduct(); + + setIsLoading(false); + + }); + + } + + }; + + const [ permissions, setPermissions ] = React.useState(Array); + + const [ checked, setChecked ] = React.useState(new Array(0).fill(true)); + + const handleChange1 = (event: React.ChangeEvent) => { + setChecked(new Array(permissions.length).fill(event.target.checked)); + }; + + const handleChangeMod = (event: React.ChangeEvent, i: number) => { + setChecked(checked.map(function(currentelement, index) { + if (index === i) { return event.target.checked; } + + return currentelement; + })); + }; + + const children = ( + + {Object.entries(permissions).map(([ key, value ]) => + handleChangeMod(event, parseInt(key)) + } + name={`${key}uniq`} />} + + label={value.name} + /> + )} + + ); + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + + + option.name} + fullWidth + disableClearable + id="combo-box-demo" + onChange={handleTenantIdChange} + value={tenantIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> +
+ num === true)} + indeterminate={checked.some((num) => num === true) && checked.some((num) => num === false)} + onChange={handleChange1} + /> + } + /> + {children} +
+
+ + + + + +
+
+ ({ + onClick: async () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const tname = r[1].getValue(); + const tdescription = r[2].getValue(); + const ttenantId = r[3].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof tname === 'string') { + setName(tname); + } else { + setName(''); + } + if (typeof tdescription === 'string') { + setDescription(tdescription); + } else { + setDescription(''); + } + if (typeof ttenantId === 'string') { + const ttenant = tenants.find((x) => x.id === parseInt(ttenantId)); + + if (ttenant) { + setTenantIdOption(ttenant); + } + setTenantId(parseInt(ttenantId)); + } else { + setTenantId(0); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('rolePermissions')).then((rp: any) => { + + const a = new Array(permissions.length).fill(false); + + rp.data.forEach((element: RolePermissions) => { + a[element.permissionId-1] = true; + }); + + setChecked(a); + }); + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + } + }} + state={{ isLoading }} + /> + ; +}; + +export default RoleTable; diff --git a/src/components/managementservice/rooms/CurrentRoom.tsx b/src/components/managementservice/rooms/CurrentRoom.tsx new file mode 100644 index 00000000..1e0040a4 --- /dev/null +++ b/src/components/managementservice/rooms/CurrentRoom.tsx @@ -0,0 +1,376 @@ +import { SyntheticEvent, useEffect, useState } from 'react'; +// eslint-disable-next-line camelcase +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, FormControlLabel, Checkbox, Autocomplete } from '@mui/material'; +import { Roles, Room, Tenant } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createRoom, getData, getRoomByName, patchData } from '../../../store/actions/managementActions'; + +const CurrentRoomModal = () => { + const dispatch = useAppDispatch(); + + type TenantOptionTypes = Array + + const [ tenants, setTenants ] = useState([ { 'id': 0, 'name': '', 'description': '' } ]); + + type RoleTypes = Array + + const [ roomExists, setRoomExists ] = useState(false); + const [ roles, setRoles ] = useState([ { 'description': 'Test', 'id': 1, 'name': 'Test', 'tenantId': 1, 'permissions': [] } ]); + const [ id, setId ] = useState(0); + const [ name, setName ] = useState(''); + const nameDisabled =false; + const [ description, setDescription ] = useState(''); + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + const [ tenantId, setTenantId ] = useState(0); + const [ defaultRoleId, setDefaultRoletId ] = useState(0); + + const [ breakoutsEnabled, setBreakoutsEnabled ] = useState(false); + const [ logo, setLogo ] = useState(''); + const [ background, setBackground ] = useState(''); + const [ maxActiveVideos, setMaxActiveVideos ] = useState(0); + const [ locked, setLocked ] = useState(false); + const [ chatEnabled, setChatEnabled ] = useState(false); + const [ raiseHandEnabled, setRaiseHandEnabled ] = useState(false); + const [ filesharingEnabled, setFilesharingEnabled ] = useState(false); + const [ localRecordingEnabled, setLocalRecordingEnabled ] = useState(false); + + const [ tenantIdOption, setTenantIdOption ] = useState(); + const [ defaultRoleIdOption, setDefaultRoleIdOption ] = useState(); + + const [ cantPatch ] = useState(false); + + async function fetchProduct() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenants')).then((tdata: any) => { + if (tdata != undefined) { + setTenants(tdata.data); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('roles')).then((tdata: any) => { + if (tdata != undefined) { + setRoles(tdata.data); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getRoomByName(window.location.pathname.substring(1))).then((tdata: any) => { + + const r = tdata.data[0] as Room; + + const tid = r.id; + const tname=r.name; + const tdescription=r.description; + const tdefaultroleId=r.defaultRoleId; + const ttenantId=r.tenantId; + const tlogo=r.logo; + const tbackground=r.background; + const tmaxActiveVideos=r.maxActiveVideos; + const tlocked=r.locked; + const tchatEnabled=r.chatEnabled; + const traiseHandEnabled=r.raiseHandEnabled; + const tfilesharingEnabled=r.filesharingEnabled; + const tlocalRecordingEnabled=r.localRecordingEnabled; + const tbreakoutsEnabled=r.breakoutsEnabled; + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof tname === 'string') { + setName(tname); + } else { + setName(''); + } + if (typeof tdescription === 'string') { + setDescription(tdescription); + } else { + setDescription(''); + } + + if (typeof tdefaultroleId === 'string') { + const tdefaultrole = roles.find((x) => x.id === parseInt(tdefaultroleId)); + + if (tdefaultrole) { + setDefaultRoleIdOption(tdefaultrole); + } else { + setDefaultRoleIdOption(undefined); + } + setDefaultRoletId(parseInt(tdefaultroleId)); + } else { + setDefaultRoletId(0); + setDefaultRoleIdOption(undefined); + } + if (typeof ttenantId === 'string') { + const ttenant = tenants.find((x) => x.id === parseInt(ttenantId)); + + if (ttenant) { + setTenantIdOption(ttenant); + } + setTenantId(parseInt(ttenantId)); + } else { + setTenantId(0); + } + + if (typeof tlogo === 'string') { + setLogo(tlogo); + } else { + setLogo(''); + } + if (typeof tbackground === 'string') { + setBackground(tbackground); + } else { + setBackground(''); + } + if (typeof tmaxActiveVideos === 'number') { + setMaxActiveVideos(tmaxActiveVideos); + } else { + setMaxActiveVideos(0); + } + + if (tlocked === true) { + setLocked(true); + } else { + setLocked(false); + } + if (tchatEnabled === true) { + setChatEnabled(true); + } else { + setChatEnabled(false); + } + if (traiseHandEnabled === true) { + setRaiseHandEnabled(true); + } else { + setRaiseHandEnabled(false); + } + if (tfilesharingEnabled === true) { + setFilesharingEnabled(true); + } else { + setFilesharingEnabled(false); + } + if (tlocalRecordingEnabled === true) { + setLocalRecordingEnabled(true); + } else { + setLocalRecordingEnabled(false); + } + if (tbreakoutsEnabled === true) { + setBreakoutsEnabled(true); + } else { + setBreakoutsEnabled(false); + } + + setOpen(true); + + }); + + } + + function checkRoomExists() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getRoomByName(window.location.pathname.substring(1))).then((tdata: any) => { + setRoomExists(tdata.total===1); + }); + } + + useEffect(() => { + // fetchProduct(); + checkRoomExists(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleNameChange = (event: { target: { value: React.SetStateAction; }; }) => { + setName(event.target.value); + }; + const handleDescriptionChange = (event: { target: { value: React.SetStateAction; }; }) => { + setDescription(event.target.value); + }; + + const handleDefaultRoleIdChange = (event: SyntheticEvent, newValue: Roles) => { + if (newValue) { + setDefaultRoletId(newValue.id); + setDefaultRoleIdOption(newValue); + } + }; + + const handleLogoChange = (event: { target: { value: React.SetStateAction; }; }) => { + setLogo(event.target.value); + }; + const handleBackgroundChange = (event: { target: { value: React.SetStateAction; }; }) => { + setBackground(event.target.value); + }; + const handleMaxActiveVideosChange = (event: { target: { value: string; }; }) => { + setMaxActiveVideos(parseInt(event.target.value)); + }; + const handleLockedChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setLocked(event.target.checked); + }; + const handleChatEnabledChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setChatEnabled(event.target.checked); + }; + const handleRaiseHandEnabledChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setRaiseHandEnabled(event.target.checked); + }; + const handleFilesharingEnabledChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setFilesharingEnabled(event.target.checked); + }; + const handleLocalRecordingEnabledChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setLocalRecordingEnabled(event.target.checked); + }; + const handleBreakoutsEnabledChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setBreakoutsEnabled(event.target.checked); + }; + const handleClose = () => { + setOpen(false); + }; + const handleOpen = () => { + fetchProduct(); + }; + const handleCreateRoom = () => { + dispatch(createRoom(window.location.pathname.substring(1))).then(() => { + checkRoomExists(); + }); + }; + + const addTenant = async () => { + + const obj : Room= { + description: description, + logo: logo, + background: background, + maxActiveVideos: maxActiveVideos, + locked: locked, + chatEnabled: chatEnabled, + raiseHandEnabled: raiseHandEnabled, + filesharingEnabled: filesharingEnabled, + localRecordingEnabled: localRecordingEnabled, + breakoutsEnabled: breakoutsEnabled, + }; + + if (defaultRoleId) { + obj.defaultRoleId=defaultRoleId; + } + dispatch(patchData(id, obj, 'rooms')).then(() => { + setOpen(false); + }); + + }; + + return <> + + Add/Edit + + + These are the parameters that you can change. + + + + + {/* */} + option.name} + fullWidth + disableClearable + onChange={handleDefaultRoleIdChange} + value={defaultRoleIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + option.name} + fullWidth + disableClearable + readOnly + // onChange={handleTenantIdChange} + value={tenantIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + + + + + + +
+ +
+ + ; +}; + +export default CurrentRoomModal; diff --git a/src/components/managementservice/rooms/Room.tsx b/src/components/managementservice/rooms/Room.tsx new file mode 100644 index 00000000..d04cd839 --- /dev/null +++ b/src/components/managementservice/rooms/Room.tsx @@ -0,0 +1,657 @@ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, FormControlLabel, Checkbox, Autocomplete } from '@mui/material'; +import React from 'react'; +import { GroupRoles, Roles, Room, RoomOwners, Tenant, User } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createRoomWithParams, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +// nested data is ok, see accessorKeys in ColumnDef below + +const RoomTable = () => { + const dispatch = useAppDispatch(); + + type TenantOptionTypes = Array + + const [ tenants, setTenants ] = useState([ { 'id': 0, 'name': '', 'description': '' } ]); + + type RoleTypes = Array + + const [ roles, setRoles ] = useState([ { 'description': 'Test', 'id': 1, 'name': 'Test', 'tenantId': 1, 'permissions': [] } ]); + + const getTenantName = (id: string): string => { + const t = tenants.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined tenant'; + } + }; + + const getRoleName = (id: string): string => { + const t = roles.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'No default role'; + } + }; + + type UserTypes = Array + + const [ users, setUsers ] = useState([ { + 'id': 0, + 'ssoId': '', + 'tenantId': 0, + 'email': '', + 'name': '', + 'avatar': '', + 'roles': [], + 'tenantAdmin': false, + 'tenantOwner': false + } ]); + + const getUserEmail = (id: number): string => { + const t = users.find((type) => type.id == id); + + if (t && t.email) { + return t.email; + } else { + return 'no such email'; + } + }; + + // should be memoized or stable + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'name', + header: 'Name' + }, + { + accessorKey: 'description', + header: 'Desc' + }, + { + accessorKey: 'createdAt', + header: 'Created at', + Cell: ({ cell }) => new Date(parseInt(cell.getValue())).toLocaleString() + }, + { + accessorKey: 'updatedAt', + header: 'Updated at', + Cell: ({ cell }) => new Date(parseInt(cell.getValue())).toLocaleString() + }, + { + accessorKey: 'creatorId', + header: 'Creator id' + }, + { + accessorKey: 'defaultRoleId', + header: 'Default Role', + Cell: ({ cell }) => getRoleName(cell.getValue()) + + }, + { + accessorKey: 'tenantId', + header: 'Tenant', + Cell: ({ cell }) => getTenantName(cell.getValue()) + }, + { + accessorKey: 'logo', + header: 'Logo' + }, + { + accessorKey: 'background', + header: 'Background' + }, + { + accessorKey: 'maxActiveVideos', + header: 'Max Active Videos', + }, + { + accessorKey: 'locked', + header: 'Locked', + Cell: ({ cell }) => + (cell.getValue() === true ? 'yes' : 'no'), + filterVariant: 'checkbox' + }, + { + accessorKey: 'chatEnabled', + header: 'Chat Enabled', + Cell: ({ cell }) => + (cell.getValue() === true ? 'yes' : 'no'), + filterVariant: 'checkbox' + }, + { + accessorKey: 'raiseHandEnabled', + header: 'Raise Hand Enabled', + Cell: ({ cell }) => + (cell.getValue() === true ? 'yes' : 'no'), + filterVariant: 'checkbox' + }, + { + accessorKey: 'filesharingEnabled', + header: 'Filesharing Enabled', + Cell: ({ cell }) => + (cell.getValue() === true ? 'yes' : 'no'), + filterVariant: 'checkbox' + }, + { + accessorKey: 'localRecordingEnabled', + header: 'Local Recording Enabled', + Cell: ({ cell }) => + (cell.getValue() === true ? 'yes' : 'no'), + filterVariant: 'checkbox' + + }, + + { + accessorKey: 'owners', + header: 'owners', + Cell: ({ cell }) => + ( + cell.getValue>().map((single:RoomOwners) => getUserEmail(single.userId)) + .join(', ') + ), + }, + { + accessorKey: 'groupRoles', + header: 'groupRoles', + Cell: ({ cell }) => + ( + cell.getValue>().map((single:GroupRoles) => single.role.description) + .join(', ') + ), + }, + { + accessorKey: 'breakoutsEnabled', + header: 'Breakouts Enabled', + Cell: ({ cell }) => + (cell.getValue() === true ? 'yes' : 'no'), + filterVariant: 'checkbox' + }, + + ], + [ tenants, roles, users ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ name, setName ] = useState(''); + const [ nameDisabled, setNameDisabled ] = useState(false); + const [ description, setDescription ] = useState(''); + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + const [ tenantId, setTenantId ] = useState(0); + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + const [ defaultRoleId, setDefaultRoletId ] = useState(0); + + const [ breakoutsEnabled, setBreakoutsEnabled ] = useState(false); + + const [ logo, setLogo ] = useState(''); + const [ background, setBackground ] = useState(''); + const [ maxActiveVideos, setMaxActiveVideos ] = useState(0); + const [ locked, setLocked ] = useState(false); + const [ chatEnabled, setChatEnabled ] = useState(false); + const [ raiseHandEnabled, setRaiseHandEnabled ] = useState(false); + const [ filesharingEnabled, setFilesharingEnabled ] = useState(false); + const [ localRecordingEnabled, setLocalRecordingEnabled ] = useState(false); + const [ tenantIdOption, setTenantIdOption ] = useState(); + const [ defaultRoleIdOption, setDefaultRoleIdOption ] = useState(); + + const [ cantPatch ] = useState(false); + const [ cantDelete ] = useState(false); + + async function fetchProduct() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('users')).then((tdata: any) => { + if (tdata != undefined) { + setUsers(tdata.data); + } + setIsLoading(false); + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenants')).then((tdata: any) => { + if (tdata != undefined) { + setTenants(tdata.data); + } + setIsLoading(false); + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('roles')).then((tdata: any) => { + if (tdata != undefined) { + setRoles(tdata.data); + } + setIsLoading(false); + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('rooms')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + setIsLoading(false); + + }); + + setIsLoading(false); + + } + + useEffect(() => { + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = React.useState(false); + + const handleClickOpen = () => { + setId(0); + setNameDisabled(false); + setName(''); + setDescription(''); + setTenantId(0); + setTenantIdOption(undefined); + setDefaultRoletId(0); + setDefaultRoleIdOption(undefined); + setLogo(''); + setBackground(''); + setMaxActiveVideos(0); + setLocked(true); + setChatEnabled(true); + setRaiseHandEnabled(true); + setFilesharingEnabled(true); + setLocalRecordingEnabled(true); + setBreakoutsEnabled(true); + + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setNameDisabled(true); + setOpen(true); + }; + + const handleNameChange = (event: { target: { value: React.SetStateAction; }; }) => { + setName(event.target.value); + }; + const handleDescriptionChange = (event: { target: { value: React.SetStateAction; }; }) => { + setDescription(event.target.value); + }; + + const handleDefaultRoleIdChange = (event: SyntheticEvent, newValue: Roles) => { + if (newValue) { + setDefaultRoletId(newValue.id); + setDefaultRoleIdOption(newValue); + } + }; + + /* const handleTenantIdChange = (event: SyntheticEvent, newValue: Tenant) => { + if (newValue) { + setTenantId(newValue.id); + setTenantIdOption(newValue); + } + }; */ + + const handleLogoChange = (event: { target: { value: React.SetStateAction; }; }) => { + setLogo(event.target.value); + }; + const handleBackgroundChange = (event: { target: { value: React.SetStateAction; }; }) => { + setBackground(event.target.value); + }; + const handleMaxActiveVideosChange = (event: { target: { value: string; }; }) => { + setMaxActiveVideos(parseInt(event.target.value)); + }; + const handleLockedChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setLocked(event.target.checked); + }; + const handleChatEnabledChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setChatEnabled(event.target.checked); + }; + const handleRaiseHandEnabledChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setRaiseHandEnabled(event.target.checked); + }; + const handleFilesharingEnabledChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setFilesharingEnabled(event.target.checked); + }; + const handleLocalRecordingEnabledChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setLocalRecordingEnabled(event.target.checked); + }; + const handleBreakoutsEnabledChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setBreakoutsEnabled(event.target.checked); + }; + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'rooms')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (name != '' && id === 0) { + + dispatch(createRoomWithParams({ + name: name, + description: description, + logo: logo, + background: background, + maxActiveVideos: maxActiveVideos, + locked: locked, + chatEnabled: chatEnabled, + raiseHandEnabled: raiseHandEnabled, + filesharingEnabled: filesharingEnabled, + localRecordingEnabled: localRecordingEnabled, + breakoutsEnabled: breakoutsEnabled + })).then(() => { + fetchProduct(); + setOpen(false); + }); + + } else if (name != '' && id != 0) { + const obj : Room= { + description: description, + logo: logo, + background: background, + maxActiveVideos: maxActiveVideos, + locked: locked, + chatEnabled: chatEnabled, + raiseHandEnabled: raiseHandEnabled, + filesharingEnabled: filesharingEnabled, + localRecordingEnabled: localRecordingEnabled, + breakoutsEnabled: breakoutsEnabled, + }; + + if (defaultRoleId) { + obj.defaultRoleId=defaultRoleId; + } + dispatch(patchData(id, obj, 'rooms')).then(() => { + fetchProduct(); + setOpen(false); + }); + + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + + + {/* */} + option.name} + fullWidth + disableClearable + onChange={handleDefaultRoleIdChange} + value={defaultRoleIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + option.name} + fullWidth + disableClearable + readOnly + // onChange={handleTenantIdChange} + value={tenantIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const tname=r[1].getValue(); + const tdescription=r[2].getValue(); + const tdefaultroleId=r[6].getValue(); + const ttenantId=r[7].getValue(); + const tlogo=r[8].getValue(); + const tbackground=r[9].getValue(); + const tmaxActiveVideos=r[10].getValue(); + const tlocked=r[11].getValue(); + const tchatEnabled=r[12].getValue(); + const traiseHandEnabled=r[13].getValue(); + const tfilesharingEnabled=r[14].getValue(); + const tlocalRecordingEnabled=r[15].getValue(); + const tbreakoutsEnabled=r[18].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof tname === 'string') { + setName(tname); + } else { + setName(''); + } + if (typeof tdescription === 'string') { + setDescription(tdescription); + } else { + setDescription(''); + } + + if (typeof tdefaultroleId === 'string') { + const tdefaultrole = roles.find((x) => x.id === parseInt(tdefaultroleId)); + + if (tdefaultrole) { + setDefaultRoleIdOption(tdefaultrole); + } else { + setDefaultRoleIdOption(undefined); + } + setDefaultRoletId(parseInt(tdefaultroleId)); + } else { + setDefaultRoletId(0); + setDefaultRoleIdOption(undefined); + } + if (typeof ttenantId === 'string') { + const ttenant = tenants.find((x) => x.id === parseInt(ttenantId)); + + if (ttenant) { + setTenantIdOption(ttenant); + } + setTenantId(parseInt(ttenantId)); + } else { + setTenantId(0); + } + + if (typeof tlogo === 'string') { + setLogo(tlogo); + } else { + setLogo(''); + } + if (typeof tbackground === 'string') { + setBackground(tbackground); + } else { + setBackground(''); + } + if (typeof tmaxActiveVideos === 'number') { + setMaxActiveVideos(tmaxActiveVideos); + } else { + setMaxActiveVideos(0); + } + + if (tlocked === true) { + setLocked(true); + } else { + setLocked(false); + } + if (tchatEnabled === true) { + setChatEnabled(true); + } else { + setChatEnabled(false); + } + if (traiseHandEnabled === true) { + setRaiseHandEnabled(true); + } else { + setRaiseHandEnabled(false); + } + if (tfilesharingEnabled === true) { + setFilesharingEnabled(true); + } else { + setFilesharingEnabled(false); + } + if (tlocalRecordingEnabled === true) { + setLocalRecordingEnabled(true); + } else { + setLocalRecordingEnabled(false); + } + if (tbreakoutsEnabled === true) { + setBreakoutsEnabled(true); + } else { + setBreakoutsEnabled(false); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + updatedAt: false, + creatorId: false, + createdAt: false, + ssoId: false, + breakoutsEnabled: false, + // tenantId: false, + logo: false, + background: false, + maxActiveVideos: false, + locked: false, + chatEnabled: false, + raiseHandEnabled: false, + filesharingEnabled: false, + localRecordingEnabled: false, + } + }} + state={{ isLoading }} + /> + ; +}; + +export default RoomTable; diff --git a/src/components/managementservice/rooms/RoomOwner.tsx b/src/components/managementservice/rooms/RoomOwner.tsx new file mode 100644 index 00000000..18613433 --- /dev/null +++ b/src/components/managementservice/rooms/RoomOwner.tsx @@ -0,0 +1,318 @@ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Autocomplete } from '@mui/material'; +import { Room, RoomOwners, User } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +const RoomOwnerTable = () => { + const dispatch = useAppDispatch(); + + type RoomOptionTypes = Array + + // nested data is ok, see accessorKeys in ColumnDef below + const [ rooms, setRooms ] = useState([ { + 'id': 1, + 'name': '', + 'description': '', + 'createdAt': '', + 'updatedAt': '', + 'creatorId': '', + 'defaultRoleId': '', + 'tenantId': 1, + 'logo': null, + 'background': null, + 'maxActiveVideos': 0, + 'locked': true, + 'chatEnabled': true, + 'raiseHandEnabled': true, + 'filesharingEnabled': true, + 'groupRoles': [], + 'localRecordingEnabled': true, + 'owners': [], + 'breakoutsEnabled': true, + } + ]); + + type UsersOptionTypes = Array + + // nested data is ok, see accessorKeys in ColumnDef below + const [ users, setUsers ] = useState([ { + 'id': 0, + 'ssoId': '', + 'tenantId': 0, + 'email': '', + 'name': '', + 'avatar': '', + 'roles': [], + 'tenantAdmin': false, + 'tenantOwner': false + } + ]); + + const getUserName = (id: string): string => { + const t = users.find((type) => type.id === parseInt(id)); + + if (t && t.email) { + return t.email; + } else { + return 'undefined user'; + } + }; + + // nested data is ok, see accessorKeys in ColumnDef below + + const getRoomName = (id: string): string => { + const t = rooms.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined room'; + } + }; + + // should be memoized or stable + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'roomId', + header: 'Room', + Cell: ({ cell }) => getRoomName(cell.getValue()) + + }, + { + accessorKey: 'userId', + header: 'User', + Cell: ({ cell }) => getUserName(cell.getValue()) + + }, + + ], + [ rooms, users ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ cantPatch, setcantPatch ] = useState(false); + const [ userIdOption, setUserIdOption ] = useState(); + const [ roomIdOption, setRoomIdOption ] = useState(); + const [ userIdOptionDisabled, setUserIdOptionDisabled ] = useState(true); + const [ roomIdOptionDisabled, setRoomIdOptionDisabled ] = useState(true); + + const [ roomId, setRoomId ] = useState(0); + const [ userId, setUserId ] = useState(0); + + async function fetchProduct() { + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('users')).then((tdata: any) => { + if (tdata != undefined) { + setUsers(tdata.data); + } + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('rooms')).then((tdata: any) => { + if (tdata != undefined) { + setRooms(tdata.data); + } + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('roomOwners')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + setIsLoading(false); + + }); + + } + + useEffect(() => { + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleClickOpen = () => { + setId(0); + setRoomId(0); + setUserId(0); + setUserIdOption(undefined); + setRoomIdOption(undefined); + setUserIdOptionDisabled(false); + setRoomIdOptionDisabled(false); + setcantPatch(false); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setUserIdOptionDisabled(true); + setRoomIdOptionDisabled(true); + setcantPatch(true); + setOpen(true); + }; + + const handleUserIdChange = (event: SyntheticEvent, newValue: User) => { + if (newValue) { + setUserId(newValue.id); + setUserIdOption(newValue); + } + }; + const handleRoomIdChange = (event: SyntheticEvent, newValue: Room) => { + if (newValue && typeof newValue.id === 'number') { + setRoomId(newValue.id); + setRoomIdOption(newValue); + } + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + dispatch(deleteData(id, 'roomOwners')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (id === 0) { + dispatch(createData({ + roomId: roomId, + userId: userId + }, 'roomOwners')).then(() => { + fetchProduct(); + setOpen(false); + + }); + } else if (id != 0) { + dispatch(patchData(id, { + roomId: roomId, + userId: userId + }, 'roomOwners')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + ((typeof option.name == 'string')?option.name:'')} + fullWidth + disableClearable + readOnly={roomIdOptionDisabled} + onChange={handleRoomIdChange} + value={roomIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + option.email} + fullWidth + disableClearable + readOnly={userIdOptionDisabled} + onChange={handleUserIdChange} + value={userIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const troomId=r[1].getValue(); + const tuserId=r[2].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof tuserId === 'string') { + const tuser = users.find((x) => x.id === parseInt(tuserId)); + + if (tuser) { + setUserIdOption(tuser); + } + setUserId(parseInt(tuserId)); + } else { + setUserId(0); + setUserIdOption(undefined); + } + + if (typeof troomId === 'string') { + const troom = rooms.find((x) => x.id === parseInt(troomId)); + + if (troom) { + setRoomIdOption(troom); + } + setRoomId(parseInt(troomId)); + } else { + setRoomId(0); + setRoomIdOption(undefined); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + } + }} + state={{ isLoading }} + /> + ; +}; + +export default RoomOwnerTable; diff --git a/src/components/managementservice/rooms/roomUserRole.tsx b/src/components/managementservice/rooms/roomUserRole.tsx new file mode 100644 index 00000000..d9791e6a --- /dev/null +++ b/src/components/managementservice/rooms/roomUserRole.tsx @@ -0,0 +1,431 @@ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Autocomplete } from '@mui/material'; +import { Roles, Room, User, UsersRoles } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +const RoomUserRoleTable = () => { + const dispatch = useAppDispatch(); + + type RoomOptionTypes = Array + + // nested data is ok, see accessorKeys in ColumnDef below + const [ rooms, setRooms ] = useState([ { + 'id': 1, + 'name': '', + 'description': '', + 'createdAt': '', + 'updatedAt': '', + 'creatorId': '', + 'defaultRoleId': '', + 'tenantId': 1, + 'logo': null, + 'background': null, + 'maxActiveVideos': 0, + 'locked': true, + 'chatEnabled': true, + 'raiseHandEnabled': true, + 'filesharingEnabled': true, + 'groupRoles': [], + 'localRecordingEnabled': true, + 'owners': [], + 'breakoutsEnabled': true, + } + ]); + + type RolesOptionTypes = Array + + // nested data is ok, see accessorKeys in ColumnDef below + const [ roles, setRoles ] = useState([ { + 'id': 0, + 'name': '', + 'description': '', + 'tenantId': 0, + 'permissions': [] + } + ]); + + const getRoleName = (id: string): string => { + const t = roles.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined role'; + } + }; + + type UsersOptionTypes = Array + + // nested data is ok, see accessorKeys in ColumnDef below + const [ users, setUsers ] = useState([ { + 'id': 0, + 'ssoId': '', + 'tenantId': 0, + 'email': '', + 'name': '', + 'avatar': '', + 'roles': [], + 'tenantAdmin': false, + 'tenantOwner': false + } + ]); + + const getUserName = (id: string): string => { + const t = users.find((type) => type.id === parseInt(id)); + + if (t && t.email) { + return t.email; + } else { + return 'undefined user'; + } + }; + + // nested data is ok, see accessorKeys in ColumnDef below + + const getRoomName = (id: string): string => { + const t = rooms.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined room'; + } + }; + + // should be memoized or stable + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'userId', + header: 'User', + Cell: ({ cell }) => getUserName(cell.getValue()) + + }, + { + accessorKey: 'roleId', + header: 'Role', + Cell: ({ cell }) => getRoleName(cell.getValue()) + + }, + { + accessorKey: 'roomId', + header: 'Room', + Cell: ({ cell }) => getRoomName(cell.getValue()) + + }, + + /* { + accessorKey: 'role', + header: 'role', + Cell: ({ cell }) => + ( + cell.getValue().name + ), + }, */ + ], + [ rooms, roles, users ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ userId, setUserId ] = useState(0); + const [ roleId, setRoleId ] = useState(0); + const [ roomId, setRoomId ] = useState(0); + + const [ cantPatch, setCantPatch ] = useState(true); + const [ cantDelete ] = useState(false); + const [ userIdOption, setUserIdOption ] = useState(); + const [ roleIdOption, setRoleIdOption ] = useState(); + const [ roomIdOption, setRoomIdOption ] = useState(); + const [ userIdOptionDisabled, setUserIdOptionDisabled ] = useState(true); + const [ roleIdOptionDisabled, setRoleIdOptionDisabled ] = useState(true); + const [ roomIdOptionDisabled, setRoomIdOptionDisabled ] = useState(true); + + async function fetchProduct() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('users')).then((tdata: any) => { + if (tdata != undefined) { + setUsers(tdata.data); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('rooms')).then((tdata: any) => { + if (tdata != undefined) { + setRooms(tdata.data); + } + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('roles')).then((tdata: any) => { + if (tdata != undefined) { + setRoles(tdata.data); + } + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('roomUserRoles')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata); + } + setIsLoading(false); + + }); + + } + + useEffect(() => { + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleClickOpen = () => { + setId(0); + setUserId(0); + setRoleId(0); + setRoomId(0); + setUserIdOption(undefined); + setRoleIdOption(undefined); + setRoomIdOption(undefined); + setUserIdOptionDisabled(false); + setRoleIdOptionDisabled(false); + setRoomIdOptionDisabled(false); + setCantPatch(false); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setCantPatch(true); + setUserIdOptionDisabled(true); + setRoleIdOptionDisabled(true); + setRoomIdOptionDisabled(true); + setOpen(true); + }; + + const handleUserIdChange = (event: SyntheticEvent, newValue: User) => { + if (newValue) { + setUserId(newValue.id); + setUserIdOption(newValue); + } + }; + const handleRoleIdChange = (event: SyntheticEvent, newValue: Roles) => { + if (newValue) { + setRoleId(newValue.id); + setRoleIdOption(newValue); + } + }; + const handleRoomIdChange = (event: SyntheticEvent, newValue: Room) => { + if (newValue && typeof newValue.id === 'number') { + setRoomId(newValue.id); + setRoomIdOption(newValue); + } + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'roomUserRoles')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (id === 0) { + dispatch(createData({ + userId: userId, + roleId: roleId, + roomId: roomId + }, 'roomUserRoles')).then(() => { + fetchProduct(); + setOpen(false); + }); + } else if (id != 0) { + dispatch(patchData(id, { + userId: userId, + roleId: roleId, + roomId: roomId + }, 'roomUserRoles')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + option.email} + fullWidth + disableClearable + readOnly={userIdOptionDisabled} + onChange={handleUserIdChange} + value={userIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + option.name} + fullWidth + disableClearable + readOnly={roleIdOptionDisabled} + onChange={handleRoleIdChange} + value={roleIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + ((typeof option.name == 'string')?option.name:'')} + fullWidth + disableClearable + readOnly={roomIdOptionDisabled} + onChange={handleRoomIdChange} + value={roomIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + {/* + + */} + + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const tuserId=r[1].getValue(); + const troleId=r[2].getValue(); + const troomId=r[3].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof tuserId === 'string') { + const tuser = users.find((x) => x.id === parseInt(tuserId)); + + if (tuser) { + setUserIdOption(tuser); + } + setUserId(parseInt(tuserId)); + } else { + setUserId(0); + setUserIdOption(undefined); + } + + if (typeof troleId === 'string') { + const troles = roles.find((x) => x.id === parseInt(troleId)); + + if (troles) { + setRoleIdOption(troles); + } + setRoleId(parseInt(troleId)); + } else { + setRoleId(0); + setRoleIdOption(undefined); + } + if (typeof troomId === 'string') { + const troom = rooms.find((x) => x.id === parseInt(troomId)); + + if (troom) { + setRoomIdOption(troom); + } + setRoomId(parseInt(troomId)); + } else { + setRoomId(0); + setRoomIdOption(undefined); + } + + handleClickOpenNoreset(); + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + } + }} + state={{ isLoading }} + /> + ; +}; + +export default RoomUserRoleTable; diff --git a/src/components/managementservice/tenants/Tenant.tsx b/src/components/managementservice/tenants/Tenant.tsx new file mode 100644 index 00000000..c9433c1c --- /dev/null +++ b/src/components/managementservice/tenants/Tenant.tsx @@ -0,0 +1,196 @@ +import React from 'react'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions } from '@mui/material'; +import { useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Tenant } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +const TenantTable = () => { + + const dispatch = useAppDispatch(); + + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'name', + header: 'Name' + }, + { + accessorKey: 'description', + header: 'description' + } + ], + [], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ name, setName ] = useState(''); + const [ description, setDescription ] = useState(''); + + async function fetchProduct() { + setIsLoading(true); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenants')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + setIsLoading(false); + + }); + } + + useEffect(() => { + fetchProduct(); + }, []); + + const [ open, setOpen ] = React.useState(false); + + const handleClickOpen = () => { + setId(0); + setName(''); + setDescription(''); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setOpen(true); + }; + + const handleNameChange = (event: { target: { value: React.SetStateAction; }; }) => { + setName(event.target.value); + }; + const handleDescriptionChange = (event: { target: { value: React.SetStateAction; }; }) => { + setDescription(event.target.value); + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'tenants')).then(() => { + fetchProduct(); + setOpen(false); + }); + + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (name != '' && id === 0) { + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(createData({ name, description }, 'tenants')).then(() => { + fetchProduct(); + setOpen(false); + }); + + } else if (name != '' && id != 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(patchData(id, { name: name, description: description }, 'tenants')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + + + + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const tname = r[1].getValue(); + const tdescription = r[2].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + + if (typeof tname === 'string') { + setName(tname); + } + + if (typeof tdescription === 'string') { + setDescription(tdescription); + } else { + setDescription(''); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: {} + }} + state={{ isLoading }} />; +}; + +export default TenantTable; diff --git a/src/components/managementservice/tenants/TenantAdmin.tsx b/src/components/managementservice/tenants/TenantAdmin.tsx new file mode 100644 index 00000000..d39e9157 --- /dev/null +++ b/src/components/managementservice/tenants/TenantAdmin.tsx @@ -0,0 +1,308 @@ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Autocomplete } from '@mui/material'; +import { Tenant, TenantOwners, User } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +const TenantAdminTable = () => { + const dispatch = useAppDispatch(); + + type TenantOptionTypes = Array + + const [ tenants, setTenants ] = useState([ { 'id': 0, 'name': '', 'description': '' } ]); + + const getTenantName = (id: string): string => { + const t = tenants.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined tenant'; + } + }; + + type UserTypes = Array + + const [ users, setUsers ] = useState([ { + 'id': 0, + 'ssoId': '', + 'tenantId': 0, + 'email': '', + 'name': '', + 'avatar': '', + 'roles': [], + 'tenantAdmin': false, + 'tenantOwner': false + } ]); + // nested data is ok, see accessorKeys in ColumnDef below + const getUserEmail = (id: string): string => { + const t = users.find((type) => type.id === parseInt(id)); + + if (t && t.email) { + return t.email; + } else { + return 'no such email'; + } + }; + // should be memoized or stable + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'tenantId', + header: 'Tenant', + Cell: ({ cell }) => getTenantName(cell.getValue()) + + }, + { + accessorKey: 'userId', + header: 'User', + Cell: ({ cell }) => getUserEmail(cell.getValue()) + + }, + ], + [ tenants, users ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ cantPatch, setcantPatch ] = useState(false); + const [ tenantId, setTenantId ] = useState(0); + const [ userId, setUserId ] = useState(0); + const [ tenantIdOption, setTenantIdOption ] = useState(); + const [ userIdOption, setUserIdOption ] = useState(); + const [ tenantIdOptionDisabled, settenantIdOptionDisabled ] = useState(false); + const [ userIdOptionDisabled, setUserIdOptionDisabled ] = useState(false); + + async function fetchProduct() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('users')).then((tdata: any) => { + if (tdata != undefined) { + setUsers(tdata.data); + } + + }); + // Find all users + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenants')).then((tdata: any) => { + if (tdata != undefined) { + setTenants(tdata.data); + } + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenantAdmins')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + setIsLoading(false); + + }); + } + + useEffect(() => { + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleClickOpen = () => { + setId(0); + setTenantId(0); + setUserId(0); + setTenantIdOption(undefined); + setUserIdOption(undefined); + settenantIdOptionDisabled(false); + setUserIdOptionDisabled(false); + setcantPatch(false); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + settenantIdOptionDisabled(true); + setUserIdOptionDisabled(true); + setcantPatch(true); + setOpen(true); + }; + + const handleTenantIdChange = (event: SyntheticEvent, newValue: Tenant) => { + if (newValue) { + setTenantId(newValue.id); + setTenantIdOption(newValue); + } + }; + const handleUserIdChange = (event: SyntheticEvent, newValue: User) => { + if (newValue) { + setUserId(newValue.id); + setUserIdOption(newValue); + } + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'tenantAdmins')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (id === 0) { + dispatch(createData({ + tenantId: tenantId, + userId: userId + }, 'tenantOwners')).then(() => { + fetchProduct(); + setOpen(false); + + }); + } else if (id != 0) { + dispatch(patchData(id, { + tenantId: tenantId, + userId: userId + }, 'tenantAdmins')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + option.email} + fullWidth + disableClearable + readOnly={userIdOptionDisabled} + onChange={handleUserIdChange} + value={userIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + option.name} + fullWidth + disableClearable + readOnly={tenantIdOptionDisabled} + onChange={handleTenantIdChange} + value={tenantIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + {/* + */} + + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const ttenantId=r[1].getValue(); + const tuserId=r[2].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof ttenantId === 'string') { + const ttenant = tenants.find((x) => x.id === parseInt(ttenantId)); + + if (ttenant) { + setTenantIdOption(ttenant); + } + setTenantId(parseInt(ttenantId)); + } else { + setTenantId(0); + } + if (typeof tuserId === 'string') { + const tuser = users.find((x) => x.id === parseInt(tuserId)); + + if (tuser) { + setUserIdOption(tuser); + } + setUserId(parseInt(tuserId)); + } else { + setUserId(0); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + } + }} + state={{ isLoading }} + /> + ; +}; + +export default TenantAdminTable; diff --git a/src/components/managementservice/tenants/TenantOAuth.tsx b/src/components/managementservice/tenants/TenantOAuth.tsx new file mode 100644 index 00000000..229af995 --- /dev/null +++ b/src/components/managementservice/tenants/TenantOAuth.tsx @@ -0,0 +1,470 @@ +/* eslint-disable camelcase */ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Autocomplete } from '@mui/material'; +import { Tenant, TenantOAuth } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; +import { notificationsActions } from '../../../store/slices/notificationsSlice'; + +const TenantOAuthTable = () => { + + const dispatch = useAppDispatch(); + + type TenantOptionTypes = Array + + const [ tenants, setTenants ] = useState([ { 'id': 0, 'name': '', 'description': '' } ]); + + const getTenantName = (id: string): string => { + const t = tenants.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined tenant'; + } + }; + + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'tenantId', + header: 'Tenant', + Cell: ({ cell }) => getTenantName(cell.getValue()) + + }, + { + accessorKey: 'access_url', + header: 'Access URL' + }, + { + accessorKey: 'authorize_url', + header: 'Authorize URL' + }, + { + accessorKey: 'profile_url', + header: 'Profile URL' + }, + { + accessorKey: 'redirect_uri', + header: 'Redirect URI' + }, + { + accessorKey: 'scope', + header: 'Scope' + }, + { + accessorKey: 'scope_delimiter', + header: 'Scope delimiter' + }, + + ], + [ tenants ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ tenantId, setTenantId ] = useState(0); + const [ profileUrl, setProfileUrl ] = useState(''); + const [ wellknown, setWellknown ] = useState(''); + const [ wellknownEpmty, setWellknownEmpty ] = useState(true); + + const [ key, setKey ] = useState(''); + const [ secret, setSecret ] = useState(''); + const [ authorizeUrl, setAuthorizeUrl ] = useState(''); + const [ accessUrl, setAccessUrl ] = useState(''); + const [ scope, setScope ] = useState(''); + const [ scopeDelimeter, setScopeDelimeter ] = useState(''); + const [ redirect, setRedirect ] = useState(''); + const [ tenantIdOption, setTenantIdOption ] = useState(); + + async function fetchProduct() { + setIsLoading(true); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenants')).then((tdata: any) => { + if (tdata != undefined) { + setTenants(tdata.data); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenantOAuths')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + }); + + setIsLoading(false); + + } + + useEffect(() => { + fetchProduct(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleClickOpen = () => { + setId(0); + setTenantId(0); + setProfileUrl('https://edumeet.example.com/kc/realms//protocol/openid-connect/userinfo'); + setKey('edumeet-dev-client'); + setSecret(''); + setAuthorizeUrl('https://edumeet.example.com/kc/realms//protocol/openid-connect/auth'); + setAccessUrl('https://edumeet.example.com/kc/realms//protocol/openid-connect/token'); + setScope('openid profile email'); + setScopeDelimeter(' '); + setTenantIdOption(undefined); + setTenantId(0); + setRedirect('https://edumeet.example.com/mgmt/oauth/tenant/callback'); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setOpen(true); + }; + + const handleTenantIdChange = (event: SyntheticEvent, newValue: Tenant) => { + if (newValue) { + setTenantId(newValue.id); + setTenantIdOption(newValue); + } + }; + + const handleProfileUrlChange = (event: { target: { value: React.SetStateAction; }; }) => { + setProfileUrl(event.target.value); + }; + + const handleWellknownChange = (event: { target: { value: React.SetStateAction; }; }) => { + setWellknown(event.target.value); + if (event.target.value != null && event.target.value.length > 10) { + setWellknownEmpty(false); + } else { + setWellknownEmpty(true); + } + + }; + + const handleWellknownUpdate = () => { + + fetch(wellknown, { + method: 'GET', + }).then(async (response) => { + if (!response.ok) { + dispatch(notificationsActions.enqueueNotification({ + message: response.statusText.toString(), + options: { variant: 'error' } + })); + } else { + const json = await response.json(); // assuming they return json + + if (json.token_endpoint!=null) + setAccessUrl(json.token_endpoint); + if (json.authorization_endpoint!=null) + setAuthorizeUrl(json.authorization_endpoint); + if (json.userinfo_endpoint!=null) + setProfileUrl(json.userinfo_endpoint); + + } + }) + .catch((error) => { + dispatch(notificationsActions.enqueueNotification({ + message: error.toString(), + options: { variant: 'error' } + })); + }); + + }; + + const handleKeyChange = (event: { target: { value: React.SetStateAction; }; }) => { + setKey(event.target.value); + }; + + const handleSecretChange = (event: { target: { value: React.SetStateAction; }; }) => { + setSecret(event.target.value); + }; + + const handleAuthorizeUrlChange = (event: { target: { value: React.SetStateAction; }; }) => { + setAuthorizeUrl(event.target.value); + }; + + const handleAccessUrlChange = (event: { target: { value: React.SetStateAction; }; }) => { + setAccessUrl(event.target.value); + }; + + const handleScopeChange = (event: { target: { value: React.SetStateAction; }; }) => { + setScope(event.target.value); + }; + + const handleScopeDelimeterChange = (event: { target: { value: React.SetStateAction; }; }) => { + setScopeDelimeter(event.target.value); + }; + + const handleRedirectChange = (event: { target: { value: React.SetStateAction; }; }) => { + setRedirect(event.target.value); + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'tenantOAuths')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (id === 0) { + + dispatch(createData({ + 'key': key, + 'secret': secret, + 'tenantId': tenantId, + 'access_url': accessUrl, + 'authorize_url': authorizeUrl, + 'profile_url': profileUrl, + 'redirect_uri': redirect, + 'scope': scope, + 'scope_delimiter': scopeDelimeter + }, 'tenantOAuths')).then(() => { + fetchProduct(); + setOpen(false); + }); + + } else if (id != 0) { + dispatch(patchData(id, { + 'tenantId': tenantId, + 'access_url': accessUrl, + 'authorize_url': authorizeUrl, + 'profile_url': profileUrl, + 'redirect_uri': redirect, + 'scope': scope, + 'scope_delimiter': scopeDelimeter }, 'tenantOAuths')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + {/* */} + option.name} + fullWidth + disableClearable + onChange={handleTenantIdChange} + value={tenantIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + + + + + + + + + + + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const ttenantId= r[1].getValue(); + const taccess= r[2].getValue(); + const tauthorize= r[3].getValue(); + const tprofile= r[4].getValue(); + const tredirect= r[5].getValue(); + const tscope= r[6].getValue(); + const tscopeDelimiter= r[7].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof ttenantId === 'string') { + const ttenant = tenants.find((x) => x.id === parseInt(ttenantId)); + + if (ttenant) { + setTenantIdOption(ttenant); + } + setTenantId(parseInt(ttenantId)); + } else { + setTenantId(0); + } + + if (typeof tprofile === 'string') { setProfileUrl(tprofile); } else { + setProfileUrl(''); + } + if (typeof tauthorize === 'string') { setAuthorizeUrl(tauthorize); } else { + setAuthorizeUrl(''); + } + if (typeof taccess === 'string') { setAccessUrl(taccess); } else { + setAccessUrl(''); + } + if (typeof tscope === 'string') { setScope(tscope); } else { + setScope(''); + } + if (typeof tscopeDelimiter === 'string') { setScopeDelimeter(tscopeDelimiter); } else { + setScopeDelimeter(''); + } + if (typeof tredirect === 'string') { setRedirect(tredirect); } else { + setRedirect(''); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + access_url: false, + authorize_url: false, + profile_url: false, + scope_delimiter: false, + } + }} + state={{ isLoading }} />; +}; + +export default TenantOAuthTable; \ No newline at end of file diff --git a/src/components/managementservice/tenants/TenantOwner.tsx b/src/components/managementservice/tenants/TenantOwner.tsx new file mode 100644 index 00000000..e25aa844 --- /dev/null +++ b/src/components/managementservice/tenants/TenantOwner.tsx @@ -0,0 +1,313 @@ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Autocomplete } from '@mui/material'; +import { Tenant, TenantOwners, User } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +const TenantOwnerTable = () => { + const dispatch = useAppDispatch(); + + type TenantOptionTypes = Array + + const [ tenants, setTenants ] = useState([ { 'id': 0, 'name': '', 'description': '' } ]); + + const getTenantName = (id: string): string => { + const t = tenants.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined tenant'; + } + }; + + type UserTypes = Array + + const [ users, setUsers ] = useState([ { + 'id': 0, + 'ssoId': '', + 'tenantId': 0, + 'email': '', + 'name': '', + 'avatar': '', + 'roles': [], + 'tenantAdmin': false, + 'tenantOwner': false + } ]); + // nested data is ok, see accessorKeys in ColumnDef below + const getUserEmail = (id: string): string => { + const t = users.find((type) => type.id === parseInt(id)); + + if (t && t.email) { + return t.email; + } else { + return 'no such email'; + } + }; + // should be memoized or stable + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'tenantId', + header: 'Tenant', + Cell: ({ cell }) => getTenantName(cell.getValue()) + + }, + { + accessorKey: 'userId', + header: 'User', + Cell: ({ cell }) => getUserEmail(cell.getValue()) + + }, + ], + [ tenants, users ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ cantPatch, setcantPatch ] = useState(false); + + const [ tenantId, setTenantId ] = useState(0); + const [ userId, setUserId ] = useState(0); + const [ tenantIdOption, setTenantIdOption ] = useState(); + const [ userIdOption, setUserIdOption ] = useState(); + const [ tenantIdOptionDisabled, settenantIdOptionDisabled ] = useState(false); + const [ userIdOptionDisabled, setUserIdOptionDisabled ] = useState(false); + + async function fetchProduct() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('users')).then((tdata: any) => { + if (tdata != undefined) { + setUsers(tdata.data); + } + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenants')).then((tdata: any) => { + if (tdata != undefined) { + setTenants(tdata.data); + } + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenantOwners')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + setIsLoading(false); + + }); + setIsLoading(false); + + } + + useEffect(() => { + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleClickOpen = () => { + setId(0); + setTenantId(0); + setUserId(0); + setTenantIdOption(undefined); + setUserIdOption(undefined); + settenantIdOptionDisabled(false); + setUserIdOptionDisabled(false); + setcantPatch(false); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + settenantIdOptionDisabled(true); + setUserIdOptionDisabled(true); + setcantPatch(true); + setOpen(true); + }; + + const handleTenantIdChange = (event: SyntheticEvent, newValue: Tenant) => { + if (newValue) { + setTenantId(newValue.id); + setTenantIdOption(newValue); + } + }; + const handleUserIdChange = (event: SyntheticEvent, newValue: User) => { + if (newValue) { + setUserId(newValue.id); + setUserIdOption(newValue); + } + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'tenantOwners')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (id === 0) { + + dispatch(createData({ + tenantId: tenantId, + userId: userId + }, 'tenantOwners')).then(() => { + fetchProduct(); + setOpen(false); + + }); + + } else if (id != 0) { + dispatch(patchData(id, { + tenantId: tenantId, + userId: userId + }, 'tenantOwners')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + option.email} + fullWidth + disableClearable + readOnly={userIdOptionDisabled} + onChange={handleUserIdChange} + value={userIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + option.name} + fullWidth + disableClearable + readOnly={tenantIdOptionDisabled} + onChange={handleTenantIdChange} + value={tenantIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + {/* + */} + + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const ttenantId=r[1].getValue(); + const tuserId=r[2].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + if (typeof ttenantId === 'string') { + const ttenant = tenants.find((x) => x.id === parseInt(ttenantId)); + + if (ttenant) { + setTenantIdOption(ttenant); + } + setTenantId(parseInt(ttenantId)); + } else { + setTenantId(0); + } + if (typeof tuserId === 'string') { + const tuser = users.find((x) => x.id === parseInt(tuserId)); + + if (tuser) { + setUserIdOption(tuser); + } + setUserId(parseInt(tuserId)); + } else { + setUserId(0); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + } + }} + state={{ isLoading }} + /> + ; +}; + +export default TenantOwnerTable; diff --git a/src/components/managementservice/tenants/TenatnFQDN.tsx b/src/components/managementservice/tenants/TenatnFQDN.tsx new file mode 100644 index 00000000..a039b85c --- /dev/null +++ b/src/components/managementservice/tenants/TenatnFQDN.tsx @@ -0,0 +1,252 @@ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Autocomplete } from '@mui/material'; +import { Tenant, TenantFQDN } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +const TenantFQDNTable = () => { + + const dispatch = useAppDispatch(); + + type TenantOptionTypes = Array + + const [ tenants, setTenants ] = useState([ { 'id': 0, 'name': '', 'description': '' } ]); + + const getTenantName = (id: string): string => { + const t = tenants.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined tenant'; + } + }; + + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'tenantId', + header: 'Tenant', + Cell: ({ cell }) => getTenantName(cell.getValue()) + + }, + { + accessorKey: 'description', + header: 'description' + }, + { + accessorKey: 'fqdn', + header: 'Fully Qualified Domain Name (FQDN)' + }, + + ], + [ tenants ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ tenantId, setTenantId ] = useState(0); + + const [ tenantIdOption, setTenantIdOption ] = useState(); + + const [ fqdn, setFQDN ] = useState(''); + + const [ description, setDescription ] = useState(''); + + async function fetchProduct() { + setIsLoading(true); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenants')).then((tdata: any) => { + if (tdata != undefined) { + setTenants(tdata.data); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenantFQDNs')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + }); + setIsLoading(false); + + } + + useEffect(() => { + fetchProduct(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleClickOpen = () => { + setId(0); + setTenantId(0); + setTenantIdOption(undefined); + setDescription(''); + setFQDN(''); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setOpen(true); + }; + + const handleTenantIdChange = (event: SyntheticEvent, newValue: Tenant) => { + if (newValue) { + setTenantId(newValue.id); + setTenantIdOption(newValue); + } + }; + + const handleDescriptionChange = (event: { target: { value: React.SetStateAction; }; }) => { + setDescription(event.target.value); + }; + const handleFQDNChange = (event: { target: { value: React.SetStateAction; }; }) => { + setFQDN(event.target.value); + }; + + const handleClose = () => { + setOpen(false); + }; + + const delTenant = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'tenantFQDNs')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addTenant = async () => { + + // add new data / mod data / error + if (id === 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(createData({ tenantId: tenantId, description: description, fqdn: fqdn }, 'tenantFQDNs')).then(() => { + fetchProduct(); + setOpen(false); + }); + } else if (id != 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(patchData(id, { name: name, description: description }, 'tenantFQDNs')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + option.name} + fullWidth + disableClearable + onChange={handleTenantIdChange} + value={tenantIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + + + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const ttenantId = r[1].getValue(); + const tdescription = r[2].getValue(); + const tfqdn = r[3].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + + if (typeof ttenantId === 'string') { + const ttenant = tenants.find((x) => x.id === parseInt(ttenantId)); + + if (ttenant) { + setTenantIdOption(ttenant); + } + setTenantId(parseInt(ttenantId)); + } else { + setTenantId(0); + } + + if (typeof tdescription === 'string') { + setDescription(tdescription); + } else { + setDescription(''); + } + if (typeof tfqdn === 'string') { + setFQDN(tfqdn); + } else { + setFQDN(''); + } + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: {} + }} + state={{ isLoading }} />; +}; + +export default TenantFQDNTable; diff --git a/src/components/managementservice/users/Users.tsx b/src/components/managementservice/users/Users.tsx new file mode 100644 index 00000000..575e6c0b --- /dev/null +++ b/src/components/managementservice/users/Users.tsx @@ -0,0 +1,368 @@ +import { SyntheticEvent, useEffect, useMemo, useState } from 'react'; +// eslint-disable-next-line camelcase +import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table'; +import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Checkbox, FormControlLabel, Autocomplete } from '@mui/material'; +import { Tenant, User } from '../../../utils/types'; +import { useAppDispatch } from '../../../store/hooks'; +import { createData, deleteData, getData, patchData } from '../../../store/actions/managementActions'; + +const UserTable = () => { + const dispatch = useAppDispatch(); + + type TenantOptionTypes = Array + + const [ tenants, setTenants ] = useState([ { 'id': 0, 'name': '', 'description': '' } ]); + + const getTenantName = (id: string): string => { + const t = tenants.find((type) => type.id === parseInt(id)); + + if (t && t.name) { + return t.name; + } else { + return 'undefined tenant'; + } + }; + + // should be memoized or stable + // eslint-disable-next-line camelcase + const columns = useMemo[]>( + () => [ + + { + accessorKey: 'id', + header: '#' + }, + { + accessorKey: 'ssoId', + header: 'ssoId' + }, + { + accessorKey: 'tenantId', + header: 'Tenant', + Cell: ({ cell }) => getTenantName(cell.getValue()) + + }, + { + accessorKey: 'email', + header: 'email' + }, + { + accessorKey: 'name', + header: 'Name' + }, + { + accessorKey: 'avatar', + header: 'avatar' + }, + { + accessorKey: 'roles', + header: 'roles' + }, + { + accessorKey: 'tenantAdmin', + header: 'Is tenant Admin?', + filterVariant: 'checkbox', + Cell: ({ cell }) => + (cell.getValue() === true ? 'yes' : 'no'), + + }, + { + accessorKey: 'tenantOwner', + Cell: ({ cell }) => + (cell.getValue() === true ? 'yes' : 'no'), + header: 'Is tenant owner?', + filterVariant: 'checkbox' + }, + ], + [ tenants ], + ); + + const [ data, setData ] = useState([]); + const [ isLoading, setIsLoading ] = useState(false); + const [ id, setId ] = useState(0); + const [ ssoId, setSsoId ] = useState(''); + const [ tenantId, setTenantId ] = useState(0); + const [ email, setEmail ] = useState(''); + const [ name, setName ] = useState(''); + const [ avatar, setAvatar ] = useState(''); + const [ tenantAdmin, setTenantAdmin ] = useState(false); + const [ tenantOwner, setTenantOwner ] = useState(false); + const [ tenantIdOption, setTenantIdOption ] = useState(); + + async function fetchProduct() { + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('tenants')).then((tdata: any) => { + if (tdata != undefined) { + setTenants(tdata.data); + } + setIsLoading(false); + + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getData('users')).then((tdata: any) => { + if (tdata != undefined) { + setData(tdata.data); + } + setIsLoading(false); + + }); + + } + + useEffect(() => { + // By moving this function inside the effect, we can clearly see the values it uses. + setIsLoading(true); + fetchProduct(); + }, []); + + const [ open, setOpen ] = useState(false); + + const handleClickOpen = () => { + setId(0); + setName(''); + setSsoId(''); + setTenantId(0); + setEmail(''); + setName(''); + setAvatar(''); + setTenantAdmin(false); + setTenantOwner(false); + setOpen(true); + }; + + const handleClickOpenNoreset = () => { + setOpen(true); + }; + const handleSsoIdChange = (event: { target: { value: React.SetStateAction; }; }) => { + setSsoId(event.target.value); + }; + const handleTenantIdChange = (event: SyntheticEvent, newValue: Tenant) => { + if (newValue) { + setTenantId(newValue.id); + setTenantIdOption(newValue); + } + }; + const handleEmailChange = (event: { target: { value: React.SetStateAction; }; }) => { + setEmail(event.target.value); + }; + const handleNameChange = (event: { target: { value: React.SetStateAction; }; }) => { + setName(event.target.value); + }; + const handleAvatarChange = (event: { target: { value: React.SetStateAction; }; }) => { + setAvatar(event.target.value); + }; + const handleTenantAdminChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setTenantAdmin(event.target.checked); + }; + const handleTenantOwnerChange = (event: { target: { checked: React.SetStateAction; }; }) => { + setTenantOwner(event.target.checked); + }; + + const handleClose = () => { + setOpen(false); + }; + + const delUser = async () => { + + // add new data / mod data / error + // eslint-disable-next-line no-alert + if (id != 0 && confirm('Are you sure?')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(deleteData(id, 'users')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + }; + + const addUser = async () => { + + // add new data / mod data / error + if (name != '' && id === 0) { + + dispatch(createData({ + ssoId: ssoId, + tenantId: tenantId, + email: email, + name: name, + avatar: avatar + }, 'users')).then(() => { + fetchProduct(); + setOpen(false); + }); + } else if (name != '' && id != 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(patchData(id, { + ssoId: ssoId, + tenantId: tenantId, + email: email, + name: name, + avatar: avatar + }, 'users')).then(() => { + fetchProduct(); + setOpen(false); + }); + } + + }; + + return <> +
+ +
+ + Add/Edit + + + These are the parameters that you can change. + + + + option.name} + fullWidth + disableClearable + id="combo-box-demo" + onChange={handleTenantIdChange} + value={tenantIdOption} + sx={{ marginTop: '8px' }} + renderInput={(params) => } + /> + + + + {/* roles */} + } label="tenantAdmin" /> + } label="tenantOwner" /> + + + + + + + + +
+ ({ + onClick: () => { + + const r = row.getAllCells(); + + const tid = r[0].getValue(); + const tssoId=r[1].getValue(); + const ttenantId=r[2].getValue(); + const temail=r[3].getValue(); + const tname=r[4].getValue(); + const tavatar=r[5].getValue(); + // const troles=r[6].getValue(); + // const ttenantAdmin=r[7].getValue(); + // const ttenantOwner=r[8].getValue(); + + if (typeof tid === 'number') { + setId(tid); + } + + if (typeof tssoId === 'string') { + setSsoId(tssoId); + } else { + setSsoId(''); + } + if (typeof ttenantId === 'string') { + const ttenant = tenants.find((x) => x.id === parseInt(ttenantId)); + + if (ttenant) { + setTenantIdOption(ttenant); + } + setTenantId(parseInt(ttenantId)); + } else { + setTenantId(0); + } + if (typeof temail === 'string') { + setEmail(temail); + } else { + setEmail(''); + } + if (typeof tname === 'string') { + setName(tname); + } else { + setName(''); + } + if (typeof tavatar === 'string') { + setAvatar(tavatar); + } else { + setAvatar(''); + } + // todo roles + /* if (ttenantAdmin === true) { + setTenantAdmin(true); + } else { + setTenantAdmin(false); + } + if (ttenantOwner === true) { + setTenantOwner(true); + } else { + setTenantOwner(false); + } */ + + handleClickOpenNoreset(); + + } + })} + columns={columns} + data={data} // fallback to array if data is undefined + initialState={{ + columnVisibility: { + avatar: false, + ssoId: false, + } + }} + state={{ isLoading }} + /> + ; +}; + +export default UserTable; diff --git a/src/components/menuitems/Login.tsx b/src/components/menuitems/Login.tsx index 74550c0c..28ce517e 100644 --- a/src/components/menuitems/Login.tsx +++ b/src/components/menuitems/Login.tsx @@ -9,16 +9,26 @@ import { logoutLabel, } from '../translated/translatedComponents'; import MoreActions from '../moreactions/MoreActions'; -import { login, logout } from '../../store/actions/permissionsActions'; +import { checkJWT, login, logout } from '../../store/actions/permissionsActions'; import { MenuItemProps } from '../floatingmenu/FloatingMenu'; +import { useEffect } from 'react'; const Login = ({ onClick }: MenuItemProps): JSX.Element => { + const dispatch = useAppDispatch(); - const loggedIn = useAppSelector((state) => state.permissions.loggedIn); + let loggedIn = useAppSelector((state) => state.permissions.loggedIn); const loginButtonLabel = loggedIn ? logoutLabel() : loginLabel(); + useEffect(() => { + + dispatch(checkJWT()).then(() => { + loggedIn = useAppSelector((state) => state.permissions.loggedIn); + }); + + }, []); + return ( { + const dispatch = useAppDispatch(); + const logo = useAppSelector((state) => state.room.logo); const loginEnabled = useAppSelector((state) => state.permissions.loginEnabled); - const loggedIn = useAppSelector((state) => state.permissions.loggedIn); + let loggedIn = useAppSelector((state) => state.permissions.loggedIn); + + useEffect(() => { + + dispatch(checkJWT()).then(() => { + loggedIn = useAppSelector((state) => state.permissions.loggedIn); + }); + + }, []); return ( { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(getUserData()).then(() => {}); + }, []); + const loggedIn = useAppSelector((state) => state.permissions.loggedIn); + + useEffect(() => { + }, [ loggedIn ]); + + // Function to render the selected component in the placeholder + const renderComponent = () => { + + if (loggedIn) { + + return <> + + + + + + + + window.open('/mgmt-admin', 'edumeet-mgmt')}> + + + + + + + + + ; + } else { + return ; + } + }; + + return ( + <> + {renderComponent()} + + ); +}; + +export default ManagementSettings; \ No newline at end of file diff --git a/src/components/settingsdialog/SettingsDialog.tsx b/src/components/settingsdialog/SettingsDialog.tsx index 4d4c6f2a..7c60f785 100644 --- a/src/components/settingsdialog/SettingsDialog.tsx +++ b/src/components/settingsdialog/SettingsDialog.tsx @@ -1,17 +1,20 @@ import { Button, Tab, Tabs } from '@mui/material'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { SettingsTab, uiActions } from '../../store/slices/uiSlice'; -import { advancedSettingsLabel, appearanceSettingsLabel, closeLabel, mediaSettingsLabel } from '../translated/translatedComponents'; +import { advancedSettingsLabel, appearanceSettingsLabel, closeLabel, managementSettingsLabel, mediaSettingsLabel } from '../translated/translatedComponents'; import CloseIcon from '@mui/icons-material/Close'; import MediaSettings from './MediaSettings'; import AppearanceSettings from './AppearanceSettings'; import GenericDialog from '../genericdialog/GenericDialog'; import AdvancedSettings from './AdvancedSettings'; +import MangagementSettings from './ManagementSettings'; +import edumeetConfig from '../../utils/edumeetConfig'; const tabs: SettingsTab[] = [ 'media', 'appearance', - 'advanced' + 'advanced', + 'management' ]; const SettingsDialog = (): JSX.Element => { @@ -35,18 +38,24 @@ const SettingsDialog = (): JSX.Element => { <> - dispatch(uiActions.setCurrentSettingsTab(tabs[value])) + onChange={(_event, value) => { + if ((!edumeetConfig.loginEnabled && tabs[value]!=='management') || edumeetConfig.loginEnabled) { + dispatch(uiActions.setCurrentSettingsTab(tabs[value])); + } + } } variant='fullWidth' > + { edumeetConfig.loginEnabled && } { currentSettingsTab === 'media' && } { currentSettingsTab === 'appearance' && } { currentSettingsTab === 'advanced' && } + { currentSettingsTab === 'management' && } + } actions={ diff --git a/src/components/settingsdialog/managementsettings/ManagementAdminLoginSettings.tsx b/src/components/settingsdialog/managementsettings/ManagementAdminLoginSettings.tsx new file mode 100644 index 00000000..dacaa164 --- /dev/null +++ b/src/components/settingsdialog/managementsettings/ManagementAdminLoginSettings.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import CssBaseline from '@mui/material/CssBaseline'; +import Container from '@mui/material/Container'; +import { adminLogin } from '../../../store/actions/permissionsActions'; +import { useAppDispatch } from '../../../store/hooks'; +import LoginButton from '../../controlbuttons/LoginButton'; + +export default function SignIn() { + + const dispatch = useAppDispatch(); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + + const email = data.get('email') as string; + const password = data.get('password') as string; + + if (email && password) { + // Authenticate with the local email/password strategy + dispatch(adminLogin(email, password)).then(() => { + // todo display success/fail + }); + } + + }; + + return ( + <> + + + + +
+ + + +
+ + + + + +
+
+ + ); +} \ No newline at end of file diff --git a/src/components/translated/translatedComponents.tsx b/src/components/translated/translatedComponents.tsx index 18e4f064..ba1ca6b4 100644 --- a/src/components/translated/translatedComponents.tsx +++ b/src/components/translated/translatedComponents.tsx @@ -513,6 +513,11 @@ export const advancedSettingsLabel = (): string => intl.formatMessage({ defaultMessage: 'Advanced' }); +export const managementSettingsLabel = (): string => intl.formatMessage({ + id: 'label.management', + defaultMessage: 'Management' +}); + export const audioInputDeviceLabel = (): string => intl.formatMessage({ id: 'settings.audioInput', defaultMessage: 'Audio input device' @@ -773,6 +778,34 @@ export const roomServerConnectionError = (message: string): string => intl.forma defaultMessage: `Room-server: ${message}` }); +export const tenantSettingsLabel = (): string => intl.formatMessage({ + id: 'label.managementTenantSettings', + defaultMessage: 'Tenant settings' +}); +export const roomSettingsLabel = (): string => intl.formatMessage({ + id: 'label.managementRoomSettings', + defaultMessage: 'Room settings' +}); +export const userSettingsLabel = (): string => intl.formatMessage({ + id: 'label.managementUserSettings', + defaultMessage: 'User settings' +}); +export const groupSettingsLabel = (): string => intl.formatMessage({ + id: 'label.managementGroupSettings', + defaultMessage: 'Group settings' +}); +export const roleSettingsLabel = (): string => intl.formatMessage({ + id: 'label.managementRoleSettings', + defaultMessage: 'Role settings' +}); +export const managementExtraSettingsLabel = (): string => intl.formatMessage({ + id: 'label.managementExtraSettings', + defaultMessage: '...' +}); +export const ruleSettingsLabel = (): string => intl.formatMessage({ + id: 'label.managementRuleSettings', + defaultMessage: 'Rule settings' +}); export const imprintLabel = (): string => intl.formatMessage({ id: 'label.imprint', defaultMessage: 'Imprint' diff --git a/src/index.tsx b/src/index.tsx index 0f1235c6..80b56d40 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,6 +15,8 @@ import { useAppDispatch } from './store/hooks'; import { setLocale } from './store/actions/localeActions'; import { CssBaseline } from '@mui/material'; import { Logger } from './utils/Logger'; +import { SnackbarProvider } from 'notistack'; +import Management from './views/management/Management'; const ErrorBoundary = lazy(() => import('./views/errorboundary/ErrorBoundary')); const App = lazy(() => import('./App')); @@ -42,6 +44,12 @@ const router = createBrowserRouter( createRoutesFromElements( <> } errorElement={} /> + + + + + } errorElement={} /> + } errorElement={} /> ), { basename } diff --git a/src/store/actions/managementActions.tsx b/src/store/actions/managementActions.tsx index 42830b43..bcf9fad9 100644 --- a/src/store/actions/managementActions.tsx +++ b/src/store/actions/managementActions.tsx @@ -1,4 +1,5 @@ import { Logger } from '../../utils/Logger'; +import { notificationsActions } from '../slices/notificationsSlice'; import { AppThunk } from '../store'; const logger = new Logger('ManagementActions'); @@ -22,13 +23,226 @@ export const getTenantFromFqdn = (fqdn: string): AppThunk> => async ( - _dispatch, + dispatch, _getState, { managementService } ): Promise => { logger.debug('createRoom() [roomName:%s]', roomName); - await (await managementService).service('rooms').create({ - name: roomName - }); + try { + await (await managementService).service('rooms').create({ + name: roomName + }); + } catch (error) { + if (error instanceof Error) { + dispatch(notificationsActions.enqueueNotification({ + message: `Failed to get data: ${error.toString()}`, + options: { variant: 'error' } + })); + } + } +}; + +export const getData = (serviceName:string): AppThunk> => async ( + dispatch, + _getState, + { managementService } +): Promise => { + + logger.debug('getData() [serviceName:%s]', serviceName); + + let data: object | undefined; + + try { + data = await (await managementService).service(serviceName).find( + { + query: { + $sort: { + id: 1 + } + } + } + ); + + } catch (error) { + if (error instanceof Error) { + dispatch(notificationsActions.enqueueNotification({ + message: `Failed to get data: ${error.toString()}`, + options: { variant: 'error' } + })); + } + } + + return data; +}; + +export const deleteData = (id : number, serviceName:string): AppThunk> => async ( + dispatch, + _getState, + { managementService } +): Promise => { + + logger.debug('deleteData() [id:%s] [serviceName:%s]', [ id, serviceName ]); + + let data: object | undefined; + + try { + data = await (await managementService).service(serviceName).remove( + id + ); + dispatch(notificationsActions.enqueueNotification({ + message: 'Delete successfull', + options: { variant: 'success' } + })); + + } catch (error) { + if (error instanceof Error) { + dispatch(notificationsActions.enqueueNotification({ + message: `Delete unsuccessful: ${error.toString()}`, + options: { variant: 'error' } + })); + } + + } + + return data; +}; + +export const createData = (params : object, serviceName:string): AppThunk> => async ( + dispatch, + _getState, + { managementService } +): Promise => { + + logger.debug('createData() [params:%s] [serviceName:%s]', [ JSON.stringify(params), serviceName ]); + + let data: object | undefined; + + try { + data = await (await managementService).service(serviceName).create( + params + ); + + dispatch(notificationsActions.enqueueNotification({ + message: 'Creation successfull', + options: { variant: 'success' } + })); + } catch (error) { + if (error instanceof Error) { + dispatch(notificationsActions.enqueueNotification({ + message: `Creation unsuccessful: ${error.toString()}`, + options: { variant: 'error' } + })); + } + } + + return data; +}; + +export const patchData = (id : number, params : object, serviceName : string): AppThunk> => async ( + dispatch, + _getState, + { managementService } +): Promise => { + + logger.debug('patchData() [id:%s] [params:%s] [serviceName:%s]', [ id, JSON.stringify(params), serviceName ]); + + let data: object | undefined; + + try { + data = await (await managementService).service(serviceName).patch( + id, + params + ); + + dispatch(notificationsActions.enqueueNotification({ + message: 'Modification successfull', + options: { variant: 'success' } + })); + } catch (error) { + if (error instanceof Error) { + dispatch(notificationsActions.enqueueNotification({ + message: `Modification unsuccessful: ${error.toString()}`, + options: { variant: 'error' } + })); + } + } + + return data; +}; + +export const getUserData = (): AppThunk> => async ( + _dispatch, + _getState, + { managementService } +): Promise => { + + logger.debug('getUserData()',); + + let data: object | undefined; + + try { + data = await (await managementService).reAuthenticate(); + } catch (error) { + + } + + return data; +}; + +export const getRoomByName = (name: string): AppThunk> => async ( + _dispatch, + _getState, + { managementService } +): Promise => { + + logger.debug('getRooms()'); + + let data: object | undefined; + + const serviceName='rooms'; + + try { + data = await (await managementService).service(serviceName).find( + { + query: { + name: name, + $sort: { + id: 1 + } + } + } + ); + } catch (error) {} + + return data; +}; + +export const createRoomWithParams = (params : object): AppThunk> => async ( + dispatch, + _getState, + { managementService } +): Promise => { + + logger.debug('createRoomWithParams()'); + + let data: object | undefined; + + const serviceName='rooms'; + + try { + data = await (await managementService).service(serviceName).create( + params + ); + + } catch (error) { + if (error instanceof Error) { + dispatch(notificationsActions.enqueueNotification({ + message: `Creation unsuccessful: ${error.toString()}`, + options: { variant: 'error' } + })); + } + } + + return data; }; diff --git a/src/store/actions/mgmtActions.tsx b/src/store/actions/mgmtActions.tsx new file mode 100644 index 00000000..2e7d2f86 --- /dev/null +++ b/src/store/actions/mgmtActions.tsx @@ -0,0 +1,39 @@ +import { AppThunk } from '../store'; +import { permissionsActions } from '../slices/permissionsSlice'; +import { Logger } from '../../utils/Logger'; + +const logger = new Logger('listenerActions'); + +// eslint-disable-next-line no-unused-vars +let messageListener: (event: MessageEvent) => void; + +export const startMGMTListeners = (): AppThunk> => async ( + dispatch, + getState, + { signalingService, managementService } +): Promise => { + logger.debug('startMGMTListeners()'); + + messageListener = async ({ data }: MessageEvent) => { + if (data.type === 'edumeet-login') { + const { data: token } = data; + + await (await managementService).authentication.setAccessToken(token); + + dispatch(permissionsActions.setToken(token)); + dispatch(permissionsActions.setLoggedIn(true)); + + if (getState().signaling.state === 'connected') + await signalingService.sendRequest('updateToken', { token }).catch((e) => logger.error('updateToken request failed [error: %o]', e)); + } + }; + + window.addEventListener('message', messageListener); +}; + +export const stopMGMTListeners = (): AppThunk> => async ( + +): Promise => { + logger.debug('stopListeners()'); + window.removeEventListener('message', messageListener); +}; diff --git a/src/store/actions/permissionsActions.tsx b/src/store/actions/permissionsActions.tsx index a3b5ec47..603fc864 100644 --- a/src/store/actions/permissionsActions.tsx +++ b/src/store/actions/permissionsActions.tsx @@ -21,6 +21,65 @@ export const login = (): AppThunk> => async ( window.open(`${config.managementUrl}/oauth/tenant?tenantId=${tenantId}`, 'loginWindow'); }; +export const adminLogin = (email: string, password:string): AppThunk> => async ( + dispatch, + getState, + { signalingService, managementService } +): Promise => { + logger.debug('adminLogin() [email s%]', email); + + const admin = await (await managementService).authenticate({ + strategy: 'local', + email: email, + password: password + }); + + if (admin) { + const accessToken = admin.accessToken; + + if (accessToken) { + const authResult = await (await managementService).authenticate({ accessToken, strategy: 'jwt' }); + + if (authResult.accessToken === accessToken) { + dispatch(permissionsActions.setToken(accessToken)); + dispatch(permissionsActions.setLoggedIn(true)); + } + } + } + if (getState().signaling.state === 'connected') + await signalingService.sendRequest('updateToken').catch((e) => logger.error('updateToken request failed [error: %o]', e)); +}; + +export const checkJWT = (): AppThunk> => async ( + dispatch, + getState, + { signalingService, managementService } +): Promise => { + logger.debug('checkJWT()'); + + const accessToken = localStorage.getItem('feathers-jwt'); + let loggedIn = false; + + /* logger.debug('checkJWT() token : %s', accessToken); */ + + if (accessToken) { + const authResult = await (await managementService).authenticate({ accessToken, strategy: 'jwt' }); + + if (authResult.accessToken === accessToken) { + loggedIn=true; + dispatch(permissionsActions.setToken(accessToken)); + } else { + await (await managementService).authentication.removeAccessToken(); + dispatch(permissionsActions.setToken()); + } + } + + dispatch(permissionsActions.setLoggedIn(loggedIn)); + + if (getState().signaling.state === 'connected') + await signalingService.sendRequest('updateToken').catch((e) => logger.error('updateToken request failed [error: %o]', e)); +}; + export const logout = (): AppThunk> => async ( dispatch, getState, diff --git a/src/store/slices/roomSlice.tsx b/src/store/slices/roomSlice.tsx index 09a1d374..9c0683d7 100644 --- a/src/store/slices/roomSlice.tsx +++ b/src/store/slices/roomSlice.tsx @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import edumeetConfig from '../../utils/edumeetConfig'; -export type RoomConnectionState = 'new' | 'lobby' | 'joined' | 'left'; +export type RoomConnectionState = 'new' | 'lobby' | 'joined' | 'left' | 'mgmt-admin'; export type RoomMode = 'P2P' | 'SFU'; export type VideoCodec = 'vp8' | 'vp9' | 'h264' | 'h265' | 'av1'; diff --git a/src/store/slices/uiSlice.tsx b/src/store/slices/uiSlice.tsx index 02faf3e9..07258bc3 100644 --- a/src/store/slices/uiSlice.tsx +++ b/src/store/slices/uiSlice.tsx @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { roomSessionsActions } from './roomSessionsSlice'; -export type SettingsTab = 'media' | 'appearance' | 'advanced'; +export type SettingsTab = 'media' | 'appearance' | 'advanced' | 'management'; export interface UiState { fullScreenConsumer?: string; diff --git a/src/utils/types.tsx b/src/utils/types.tsx index 735544ea..fab7e8e5 100644 --- a/src/utils/types.tsx +++ b/src/utils/types.tsx @@ -265,3 +265,128 @@ export interface HTMLMediaElementWithSink extends HTMLMediaElement { // eslint-disable-next-line no-unused-vars setSinkId(deviceId: string): Promise } + +export type Tenant = { + id: number, + name: string, + description: string +}; + +export type TenantFQDN = { + id: number, + tenantId: number, + description: string, + fqdn: string +}; + +export type TenantOAuth = { + id: number, + tenantId: number, + access_url: string, + authorize_url: string, + profile_url: string, + redirect_uri: string, + scope: string, + scope_delimiter: string, +}; + +export type User = { + id: number, + ssoId: string, + tenantId: number, + email: string, + name: string, + avatar: string, + roles: [], + tenantAdmin: boolean, + tenantOwner: boolean +}; + +export type Roles = { + id: number, + name: string, + description: string, + tenantId: number + permissions: Array +}; + +export type GroupRoles = { + id: number, + groupId: number, + role:Roles, + roleId:number, + roomId:number +}; + +export type UsersRoles = { + id: number, + userId: number, + role:Roles, + roleId:number, + roomId:number +}; + +export type RoomOwners = { + id: number, + roomId: number, + userId: number, +}; +export type TenantOwners = { + id: number, + tenantId: number, + userId: number, +}; + +export type TenantAdmins = { + id: number, + tenantId: number, + userId: number, +}; + +export type Permissions = { + id: number, + name: string, + description: string, +}; + +export type RolePermissions = { + id: number, + permission: Permissions + permissionId: number, + roleId: number, +}; + +export type Room = { + id?: number, + name?: string, + description: string, + createdAt?: string, + updatedAt?: string, + creatorId?: string, + defaultRoleId? : number | string, + tenantId?: number | null, + logo: string | null, + background: string | null, + maxActiveVideos: number, + locked: boolean, + chatEnabled: boolean, + raiseHandEnabled: boolean, + filesharingEnabled: boolean, + groupRoles?: Array, + localRecordingEnabled: boolean, + owners?: Array, + breakoutsEnabled: boolean, +}; + +export type Groups = { + id: number, + name: string, + description: string, + tenantId: number +}; + +export type GroupUsers = { + id: number, + groupId: number, + userId: number +}; diff --git a/src/views/join/Join.tsx b/src/views/join/Join.tsx index 8f81def9..dc3cc3df 100644 --- a/src/views/join/Join.tsx +++ b/src/views/join/Join.tsx @@ -19,10 +19,8 @@ import AudioOutputChooser from '../../components/devicechooser/AudioOutputChoose import { canSelectAudioOutput } from '../../store/selectors'; import TestAudioOutputButton from '../../components/audiooutputtest/AudioOutputTest'; import edumeetConfig from '../../utils/edumeetConfig'; -import ImpressumButton from '../../components/controlbuttons/ImpressumButton'; import MicVolume from '../../components/volume/MicVolume'; - interface JoinProps { roomId: string; } diff --git a/src/views/management/Management.tsx b/src/views/management/Management.tsx new file mode 100644 index 00000000..589aac0e --- /dev/null +++ b/src/views/management/Management.tsx @@ -0,0 +1,316 @@ +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import CssBaseline from '@mui/material/CssBaseline'; +import Divider from '@mui/material/Divider'; +import Drawer from '@mui/material/Drawer'; +import IconButton from '@mui/material/IconButton'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import LogoutIcon from '@mui/icons-material/Logout'; +import TenantTable from '../../components/managementservice/tenants/Tenant'; +import TenantFQDNTable from '../../components/managementservice/tenants/TenatnFQDN'; +import TenantOAuthTable from '../../components/managementservice/tenants/TenantOAuth'; +import { useEffect, useState } from 'react'; +import RoomTable from '../../components/managementservice/rooms/Room'; +import GroupTable from '../../components/managementservice/groups/Groups'; +import UserTable from '../../components/managementservice/users/Users'; +import RoleTable from '../../components/managementservice/role/Role'; +import MenuIcon from '@mui/icons-material/Menu'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import PersonSearchIcon from '@mui/icons-material/PersonSearch'; +import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount'; +import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; +import PeopleOutlineIcon from '@mui/icons-material/PeopleOutline'; +import PersonOutlineIcon from '@mui/icons-material/PersonOutline'; +import MeetingRoomIcon from '@mui/icons-material/MeetingRoom'; +import { getUserData } from '../../store/actions/managementActions'; +import { useAppDispatch, useAppSelector, useNotifier } from '../../store/hooks'; +import PermissionTable from '../../components/managementservice/permisssion/Permission'; +import InfoIcon from '@mui/icons-material/Info'; +import TenantAdminTable from '../../components/managementservice/tenants/TenantAdmin'; +import TenantOwnerTable from '../../components/managementservice/tenants/TenantOwner'; +import { checkJWT, logout } from '../../store/actions/permissionsActions'; +import SignIn from '../../components/settingsdialog/managementsettings/ManagementAdminLoginSettings'; +import { startMGMTListeners, stopMGMTListeners } from '../../store/actions/mgmtActions'; +import RoomOwnerTable from '../../components/managementservice/rooms/RoomOwner'; +import GroupRoleTable from '../../components/managementservice/groups/GroupRole'; +import GroupUserTable from '../../components/managementservice/groups/GroupUser'; +import RoomUserRoleTable from '../../components/managementservice/rooms/roomUserRole'; + +const drawerWidth = 300; + +export default function ManagementUI(/* props: Props */) { + useNotifier(); + + const dispatch = useAppDispatch(); + + const [ mobileOpen, setMobileOpen ] = useState(false); + + const handleDrawerToggle = () => { + setMobileOpen(!mobileOpen); + }; + + const [ selectedComponent, setSelectedComponent ] = useState(''); + const [ username, setUsername ] = useState(''); + const loggedIn = useAppSelector((state) => state.permissions.loggedIn); + + useEffect(() => { + dispatch(startMGMTListeners()); + + dispatch(checkJWT()).then(() => { + }); + + return () => { + dispatch(stopMGMTListeners()); + }; + }, []); + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(getUserData()).then((tdata: any) => { + if (tdata) { + setUsername(tdata.user.email); + } + }); + }, [ loggedIn ]); + + // Function to render the selected component in the placeholder + const renderComponent = () => { + + if (loggedIn) { + switch (selectedComponent) { + case 'login': + return
+ +
; + case 'tenant': + return <> + Tenant settings + + Tenant domain settings + + Tenant authentication source + + Tenant admins + + Tenants owners + + ; + case 'room': + return <> + Room settings + + Room owners + + Room user roles + + ; + case 'user': + return <> + User data + + ; + case 'group': + return <> + Group table + + Group roles + + Group user roles + + ; + case 'role': + return <>Roles + + ; + case 'permission': + return <>Permissions; + default: + return Select an item to load a component ; + } + } else { + return ; + } + }; + + const drawer = ( +
+ + + + + + { if (!loggedIn) { setSelectedComponent('login'); } } + }> + + + + + + + + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispatch(logout()).then(() => { + window.location.reload(); + }); + } + }> + + + + + + + + + + + + + setSelectedComponent('permission') + }> + + + + + + + + setSelectedComponent('tenant') + }> + + + + + + + + setSelectedComponent('room') + }> + + + + + + + + setSelectedComponent('user') + }> + + + + + + + + setSelectedComponent('group') + }> + + + + + + + + setSelectedComponent('role') + }> + + + + + + + + + +
+ ); + + return ( + <> + + + + + + + + + + Edumeet management client + + + + + {/* The implementation can be swapped with js to avoid SEO duplication of links. */} + + {drawer} + + + {drawer} + + + + +
+ {renderComponent()} +
+ +
+ +
+ + ); +} diff --git a/yarn.lock b/yarn.lock index 45960f0a..2d9fbf22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -150,13 +150,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.25.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.7": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.9.tgz#65884fd6dc255a775402cc1d9811082918f4bf00" + integrity sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.25.0": version "7.25.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" @@ -210,7 +217,7 @@ source-map "^0.5.7" stylis "4.2.0" -"@emotion/cache@^11.11.0", "@emotion/cache@^11.13.0", "@emotion/cache@^11.13.1": +"@emotion/cache@^11.13.0", "@emotion/cache@^11.13.1": version "11.13.1" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.1.tgz#fecfc54d51810beebf05bf2a161271a1a91895d7" integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw== @@ -252,7 +259,7 @@ "@emotion/weak-memoize" "^0.4.0" hoist-non-react-statics "^3.3.1" -"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0", "@emotion/serialize@^1.3.1": +"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0", "@emotion/serialize@^1.3.1", "@emotion/serialize@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.2.tgz#e1c1a2e90708d5d85d81ccaee2dfeb3cc0cccf7a" integrity sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA== @@ -528,6 +535,14 @@ ajv-formats "^3.0.1" json-schema-to-ts "^3.1.1" +"@feathersjs/socketio-client@^5.0.30": + version "5.0.30" + resolved "https://registry.yarnpkg.com/@feathersjs/socketio-client/-/socketio-client-5.0.30.tgz#1d79a39f74937f03ff9123149fb737eea1a6dc60" + integrity sha512-hXGXU73J4cX66F68HUvJ18s2nl8OztZqFCijh0mo7bP//V19BI3NhMjrU6i/+wCLnGf3MQyesl08lQFQFmhukw== + dependencies: + "@feathersjs/feathers" "^5.0.30" + "@feathersjs/transport-commons" "^5.0.30" + "@feathersjs/transport-commons@^5.0.30": version "5.0.30" resolved "https://registry.yarnpkg.com/@feathersjs/transport-commons/-/transport-commons-5.0.30.tgz#5fdd48ca6c0c296019add355e02484d4770e2c80" @@ -660,86 +675,109 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@mui/core-downloads-tracker@^5.16.7": - version "5.16.7" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz#182a325a520f7ebd75de051fceabfc0314cfd004" - integrity sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ== +"@mui/core-downloads-tracker@^6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.5.tgz#96fe5068d55fba27d90421b8265b965c203d09e2" + integrity sha512-3J96098GrC95XsLw/TpGNMxhUOnoG9NZ/17Pfk1CrJj+4rcuolsF2RdF3XAFTu/3a/A+5ouxlSIykzYz6Ee87g== -"@mui/icons-material@^5.16.7": - version "5.16.7" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.16.7.tgz#e27f901af792065efc9f3d75d74a66af7529a10a" - integrity sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q== +"@mui/icons-material@^6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-6.1.5.tgz#5b5a4237796956e208d480ebd68350a78d81d202" + integrity sha512-SbxFtO5I4cXfvhjAMgGib/t2lQUzcEzcDFYiRHRufZUeMMeXuoKaGsptfwAHTepYkv0VqcCwvxtvtWbpZLAbjQ== dependencies: - "@babel/runtime" "^7.23.9" + "@babel/runtime" "^7.25.7" -"@mui/material@^5.16.7": - version "5.16.7" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.16.7.tgz#6e814e2eefdaf065a769cecf549c3569e107a50b" - integrity sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg== +"@mui/material@^6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.1.5.tgz#1ba8deda18564b277f37d957d523a9d2624b4b9a" + integrity sha512-rhaxC7LnlOG8zIVYv7BycNbWkC5dlm9A/tcDUp0CuwA7Zf9B9JP6M3rr50cNKxI7Z0GIUesAT86ceVm44quwnQ== dependencies: - "@babel/runtime" "^7.23.9" - "@mui/core-downloads-tracker" "^5.16.7" - "@mui/system" "^5.16.7" - "@mui/types" "^7.2.15" - "@mui/utils" "^5.16.6" + "@babel/runtime" "^7.25.7" + "@mui/core-downloads-tracker" "^6.1.5" + "@mui/system" "^6.1.5" + "@mui/types" "^7.2.18" + "@mui/utils" "^6.1.5" "@popperjs/core" "^2.11.8" - "@types/react-transition-group" "^4.4.10" - clsx "^2.1.0" + "@types/react-transition-group" "^4.4.11" + clsx "^2.1.1" csstype "^3.1.3" prop-types "^15.8.1" react-is "^18.3.1" react-transition-group "^4.4.5" -"@mui/private-theming@^5.16.6": - version "5.16.6" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.16.6.tgz#547671e7ae3f86b68d1289a0b90af04dfcc1c8c9" - integrity sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw== +"@mui/private-theming@^6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-6.1.5.tgz#634166d5793f6635ee2f815fb30f03342ff4df41" + integrity sha512-FJqweqEXk0KdtTho9C2h6JEKXsOT7MAVH2Uj3N5oIqs6YKxnwBn2/zL2QuYYEtj5OJ87rEUnCfFic6ldClvzJw== dependencies: - "@babel/runtime" "^7.23.9" - "@mui/utils" "^5.16.6" + "@babel/runtime" "^7.25.7" + "@mui/utils" "^6.1.5" prop-types "^15.8.1" -"@mui/styled-engine@^5.16.6": - version "5.16.6" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.16.6.tgz#60110c106dd482dfdb7e2aa94fd6490a0a3f8852" - integrity sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g== +"@mui/styled-engine@^6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.1.5.tgz#a21a75799f84446e3553ab191a891ca2192933cc" + integrity sha512-tiyWzMkHeWlOoE6AqomWvYvdml8Nv5k5T+LDwOiwHEawx8P9Lyja6ZwWPU6xljwPXYYPT2KBp1XvMly7dsK46A== dependencies: - "@babel/runtime" "^7.23.9" - "@emotion/cache" "^11.11.0" + "@babel/runtime" "^7.25.7" + "@emotion/cache" "^11.13.1" + "@emotion/serialize" "^1.3.2" + "@emotion/sheet" "^1.4.0" csstype "^3.1.3" prop-types "^15.8.1" -"@mui/system@^5.16.7": - version "5.16.7" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.16.7.tgz#4583ca5bf3b38942e02c15a1e622ba869ac51393" - integrity sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA== - dependencies: - "@babel/runtime" "^7.23.9" - "@mui/private-theming" "^5.16.6" - "@mui/styled-engine" "^5.16.6" - "@mui/types" "^7.2.15" - "@mui/utils" "^5.16.6" - clsx "^2.1.0" +"@mui/system@^6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.1.5.tgz#2124f43be98a7393e08edf89ae0fbc8678607e11" + integrity sha512-vPM9ocQ8qquRDByTG3XF/wfYTL7IWL/20EiiKqByLDps8wOmbrDG9rVznSE3ZbcjFCFfMRMhtxvN92bwe/63SA== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/private-theming" "^6.1.5" + "@mui/styled-engine" "^6.1.5" + "@mui/types" "^7.2.18" + "@mui/utils" "^6.1.5" + clsx "^2.1.1" csstype "^3.1.3" prop-types "^15.8.1" -"@mui/types@^7.2.15": - version "7.2.17" - resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.17.tgz#329826062d4079de5ea2b97007575cebbba1fdbc" - integrity sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ== +"@mui/types@^7.2.18": + version "7.2.18" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.18.tgz#4b6385ed2f7828ef344113cdc339d6fdf8e4bc23" + integrity sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg== -"@mui/utils@^5.16.6": - version "5.16.6" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.16.6.tgz#905875bbc58d3dcc24531c3314a6807aba22a711" - integrity sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA== +"@mui/utils@^5.16.6 || ^6.0.0", "@mui/utils@^6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.1.5.tgz#a5c75ac48f9913340670ebeba2907568a6ee8c49" + integrity sha512-vp2WfNDY+IbKUIGg+eqX1Ry4t/BilMjzp6p9xO1rfqpYjH1mj8coQxxDfKxcQLzBQkmBJjymjoGOak5VUYwXug== dependencies: - "@babel/runtime" "^7.23.9" - "@mui/types" "^7.2.15" - "@types/prop-types" "^15.7.12" + "@babel/runtime" "^7.25.7" + "@mui/types" "^7.2.18" + "@types/prop-types" "^15.7.13" clsx "^2.1.1" prop-types "^15.8.1" react-is "^18.3.1" +"@mui/x-date-pickers@^7.22.0": + version "7.22.0" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.22.0.tgz#f0d7eae779f31be98a146727db8304ee77293bf5" + integrity sha512-hopYo3ORP7ddYKnyBsqAtO2txEe2Zf6cehdikS5b1cqMTGOSL+18b11jfGVod9oipjb9L2JcT/WWkjoifs9Iww== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0" + "@mui/x-internals" "7.21.0" + "@types/react-transition-group" "^4.4.11" + clsx "^2.1.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + +"@mui/x-internals@7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.21.0.tgz#daca984059015b27efdb47bb44dc7ff4a6816673" + integrity sha512-94YNyZ0BhK5Z+Tkr90RKf47IVCW8R/1MvdUhh6MCQg6sZa74jsX+x+gEZ4kzuCqOsuyTyxikeQ8vVuCIQiP7UQ== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -822,6 +860,37 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== +"@tanstack/match-sorter-utils@8.19.4": + version "8.19.4" + resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz#dacf772b5d94f4684f10dbeb2518cf72dccab8a5" + integrity sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg== + dependencies: + remove-accents "0.5.0" + +"@tanstack/react-table@8.20.5": + version "8.20.5" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.5.tgz#19987d101e1ea25ef5406dce4352cab3932449d8" + integrity sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA== + dependencies: + "@tanstack/table-core" "8.20.5" + +"@tanstack/react-virtual@3.10.6": + version "3.10.6" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.10.6.tgz#f90f97d50a8d83dcd3c3a2d425aadbb55d4837db" + integrity sha512-xaSy6uUxB92O8mngHZ6CvbhGuqxQ5lIZWCBy+FjhrbHmOwc6BnOnKkYm2FsB1/BpKw/+FVctlMbEtI+F6I1aJg== + dependencies: + "@tanstack/virtual-core" "3.10.6" + +"@tanstack/table-core@8.20.5": + version "8.20.5" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d" + integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg== + +"@tanstack/virtual-core@3.10.6": + version "3.10.6" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.6.tgz#babe3989b2344a5f12fc64129f9bbed5d3402999" + integrity sha512-1giLc4dzgEKLMx5pgKjL6HlG5fjZMgCjzlKAlpr7yoUtetVPELgER1NtephAI910nMwfPTHNyWKSFmJdHkz2Cw== + "@thaunknown/simple-peer@^10.0.10", "@thaunknown/simple-peer@^10.0.8": version "10.0.10" resolved "https://registry.yarnpkg.com/@thaunknown/simple-peer/-/simple-peer-10.0.10.tgz#59cc7f5dcc748ab008a580e4a1536f6363d1dbc4" @@ -1015,7 +1084,7 @@ "@types/node" "*" "@types/parse-torrent-file" "*" -"@types/prop-types@*", "@types/prop-types@^15.7.12": +"@types/prop-types@*", "@types/prop-types@^15.7.13": version "15.7.13" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== @@ -1025,10 +1094,10 @@ resolved "https://registry.yarnpkg.com/@types/random-string/-/random-string-0.0.30.tgz#0eac646f771227ef95a009a1c0a5dcc236339f6a" integrity sha512-Wc6sEDQqQz8YgRZZIP6jzeGx5TzUiOlaX2R/fqs56PAgdH3r8j3SUCjawdsA2yDqB+KgVTIjXtgpTCML6IEOMw== -"@types/react-dom@^18.3.0": - version "18.3.0" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" - integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== +"@types/react-dom@^18.3.1": + version "18.3.1" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" + integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== dependencies: "@types/react" "*" @@ -1042,14 +1111,14 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react-transition-group@^4.4.10": +"@types/react-transition-group@^4.4.11": version "4.4.11" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" integrity sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@^18.3.8": +"@types/react@*", "@types/react@16 || 17 || 18": version "18.3.8" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.8.tgz#1672ab19993f8aca7c7dc844c07d5d9e467d5a79" integrity sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q== @@ -1057,6 +1126,14 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/react@^18.3.12": + version "18.3.12" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60" + integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/redux-logger@^3.0.13": version "3.0.13" resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.13.tgz#473e98428cdcc6dc93c908de66732bf932e36bc8" @@ -1722,7 +1799,7 @@ clsx@^1.1.0: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== -clsx@^2.1.0, clsx@^2.1.1: +clsx@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== @@ -2734,6 +2811,11 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" +highlight-words@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/highlight-words/-/highlight-words-1.2.2.tgz#9875b75d11814d7356b24f23feeb7d77761fa867" + integrity sha512-Mf4xfPXYm8Ay1wTibCrHpNWeR2nUMynMVFkXCi4mbl+TEgmNOe+I4hV7W3OCZcSvzGL6kupaqpfHOemliMTGxQ== + hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -3345,6 +3427,16 @@ marked@^9.1.6: resolved "https://registry.yarnpkg.com/marked/-/marked-9.1.6.tgz#5d2a3f8180abfbc5d62e3258a38a1c19c0381695" integrity sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q== +material-react-table@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/material-react-table/-/material-react-table-3.0.1.tgz#a6d592a1e370acfd453c37f1deaa870c47e7bf5b" + integrity sha512-RP+bnpsOAH5j6zwP04u9HB37fyqbd6mVv9mkT4IUJC3e3gEqixZmkNdJMVM1ZVHoq7yIaM381xf22mpBVe0IaA== + dependencies: + "@tanstack/match-sorter-utils" "8.19.4" + "@tanstack/react-table" "8.20.5" + "@tanstack/react-virtual" "3.10.6" + highlight-words "1.2.2" + mediasoup-client@^3.7.16: version "3.7.16" resolved "https://registry.yarnpkg.com/mediasoup-client/-/mediasoup-client-3.7.16.tgz#42b5f8062a618c36d1dba6332a5ecfb4105bd7c1" @@ -4023,6 +4115,11 @@ rematrix@0.2.2: resolved "https://registry.yarnpkg.com/rematrix/-/rematrix-0.2.2.tgz#c96a050260782db9908904885991256e23beda5d" integrity sha512-agFFS3RzrLXJl5LY5xg/xYyXvUuVAnkhgKO7RaO9J1Ssth6yvbO+PIiV67V59MB5NCdAK2flvGvNT4mdKVniFA== +remove-accents@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687" + integrity sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A== + require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"