diff --git a/package-lock.json b/package-lock.json
index fdaa17a4..0d599ce1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"eslint-plugin-prettier": "^5.1.3",
"express": "4.18.3",
"express-http-proxy": "^2.0.0",
+ "http-proxy-middleware": "^2.0.6",
"http-status-codes": "^2.3.0",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
@@ -1679,6 +1680,14 @@
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
},
+ "node_modules/@types/http-proxy": {
+ "version": "1.17.14",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz",
+ "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2939,6 +2948,11 @@
"node": ">=6"
}
},
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
+ },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -3280,6 +3294,25 @@
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -3515,6 +3548,42 @@
"node": ">= 0.8"
}
},
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-proxy-middleware": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
+ "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
+ "dependencies": {
+ "@types/http-proxy": "^1.17.8",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "is-plain-obj": "^3.0.0",
+ "micromatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "@types/express": "^4.17.13"
+ },
+ "peerDependenciesMeta": {
+ "@types/express": {
+ "optional": true
+ }
+ }
+ },
"node_modules/http-status-codes": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
@@ -3680,6 +3749,17 @@
"node": ">=8"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+ "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4003,7 +4083,6 @@
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
- "dev": true,
"dependencies": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
@@ -4727,6 +4806,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -6491,6 +6575,14 @@
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
},
+ "@types/http-proxy": {
+ "version": "1.17.14",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz",
+ "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -7383,6 +7475,11 @@
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
+ "eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
+ },
"events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -7688,6 +7785,11 @@
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
},
+ "follow-redirects": {
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
+ },
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -7852,6 +7954,28 @@
"toidentifier": "1.0.1"
}
},
+ "http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "requires": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "http-proxy-middleware": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
+ "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
+ "requires": {
+ "@types/http-proxy": "^1.17.8",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "is-plain-obj": "^3.0.0",
+ "micromatch": "^4.0.2"
+ }
+ },
"http-status-codes": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
@@ -7967,6 +8091,11 @@
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="
},
+ "is-plain-obj": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+ "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="
+ },
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -8254,7 +8383,6 @@
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
- "dev": true,
"requires": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
@@ -8742,6 +8870,11 @@
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
},
+ "requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
+ },
"resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
diff --git a/package.json b/package.json
index c4e936eb..8a0a70a3 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"eslint-plugin-prettier": "^5.1.3",
"express": "4.18.3",
"express-http-proxy": "^2.0.0",
+ "http-proxy-middleware": "^2.0.6",
"http-status-codes": "^2.3.0",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
diff --git a/server.js b/server.js
index d28f5bc2..1e4129d2 100644
--- a/server.js
+++ b/server.js
@@ -13,10 +13,13 @@ const app = express()
const PORT = process.env.PORT || 3000
const DAPLA_TEAM_API_URL = process.env.DAPLA_TEAM_API_URL || 'https://dapla-team-api-v2.staging-bip-app.ssb.no'
-// Proxy, note this middleware must be place before all else.. THIS TOOK ME 3 HOURS TO FIGURE OUT! TODO: Remove comment
app.use(
'/api',
proxy(DAPLA_TEAM_API_URL, {
+ proxyReqBodyDecorator: function (bodyContent, srcReq) {
+ console.log(`Request Body: ${bodyContent}`)
+ return bodyContent
+ },
proxyReqOptDecorator: function (proxyReqOpts, srcReq) {
console.log(`Request Headers:`, srcReq.headers)
if (srcReq.body) {
@@ -34,6 +37,10 @@ app.use(
console.log(`Response Headers:`, proxyRes.headers)
return proxyResData
},
+ proxyErrorHandler: function (err, res) {
+ console.error('Proxy Error:', err)
+ res.status(500).send('Proxy Error')
+ },
})
)
@@ -66,6 +73,39 @@ async function fetchPhoto(accessToken, url, fallbackErrorMessage) {
return photoBuffer.toString('base64')
}
+//TODO: Remove me once DELETE with proxy is fixed
+app.delete('/localApi/groups/:groupUniformName/:userPrincipalName', async (req, res) => {
+ const token = req.headers.authorization
+ const groupUniformName = req.params.groupUniformName
+ const userPrincipalName = req.params.userPrincipalName
+ const groupsUrl = `${DAPLA_TEAM_API_URL}/groups/${groupUniformName}/users`
+
+ try {
+ const response = await fetch(groupsUrl, {
+ method: 'DELETE',
+ headers: {
+ Accept: '*/*',
+ 'Content-Type': 'application/json',
+ Authorization: token,
+ },
+ body: JSON.stringify({
+ users: [userPrincipalName],
+ }),
+ })
+
+ if (!response.ok) {
+ const err = await response.text()
+ res.status(response.status).send(err)
+ } else {
+ const data = await response.json()
+ res.status(response.status).send(data)
+ }
+ } catch (error) {
+ console.log(error)
+ res.status(500).send('Internal Server Error')
+ }
+})
+
app.get('/localApi/fetch-token', (req, res) => {
if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer')) {
return res.status(401).json({ message: 'No token provided' })
diff --git a/src/components/DeleteLink/DeleteLink.tsx b/src/components/DeleteLink/DeleteLink.tsx
new file mode 100644
index 00000000..a6c0f5ec
--- /dev/null
+++ b/src/components/DeleteLink/DeleteLink.tsx
@@ -0,0 +1,21 @@
+import styles from './deletelink.module.scss'
+
+import { Trash2 } from 'react-feather'
+
+interface DeleteLink {
+ children: string
+ tabIndex?: number
+ icon?: boolean
+ handleDeleteUser: CallableFunction
+}
+
+const DeleteLink = ({ children, tabIndex, icon, handleDeleteUser }: DeleteLink) => {
+ return (
+ handleDeleteUser}>
+ {icon && }
+ {children}
+
+ )
+}
+
+export default DeleteLink
diff --git a/src/components/DeleteLink/deletelink.module.scss b/src/components/DeleteLink/deletelink.module.scss
new file mode 100644
index 00000000..7ff82185
--- /dev/null
+++ b/src/components/DeleteLink/deletelink.module.scss
@@ -0,0 +1,22 @@
+@use '@statisticsnorway/ssb-component-library/src/style/variables' as variables;
+
+.deleteLinkWrapper {
+ display: inline-flex;
+ align-items: center;
+ color: variables.$ssb-red-3;
+ padding: .5rem;
+ line-height: 1.7;
+ cursor: pointer;
+
+ svg {
+ margin-right: .5rem;
+ }
+
+ span {
+ border-bottom: 1px solid;
+ }
+
+ &:focus {
+ @include variables.focus-marker;
+ }
+}
\ No newline at end of file
diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx
index 921baa00..b022e732 100644
--- a/src/components/Table/Table.tsx
+++ b/src/components/Table/Table.tsx
@@ -14,11 +14,13 @@ interface TableProps extends TableData {
interface TableDesktopViewProps extends TableData {
activeTab?: string
}
+
export interface TableData {
columns: {
id: string
label: string
unsortable?: boolean
+ align?: string
}[]
data: {
id: string
@@ -152,7 +154,9 @@ const TableDesktopView = ({ columns, data, activeTab }: TableDesktopViewProps) =
return (
{columns.map((column) => (
- {row[column.id]} |
+
+ {row[column.id]}
+ |
))}
)
diff --git a/src/components/Table/table.module.scss b/src/components/Table/table.module.scss
index e66ad532..830d20a1 100644
--- a/src/components/Table/table.module.scss
+++ b/src/components/Table/table.module.scss
@@ -134,4 +134,11 @@
@media #{variables.$mobile} {
display: flex;
}
+}
+
+.centerText {
+ span {
+ display: flex;
+ justify-content: center;
+ }
}
\ No newline at end of file
diff --git a/src/pages/TeamDetail/TeamDetail.tsx b/src/pages/TeamDetail/TeamDetail.tsx
index 4dd57ab3..476268f6 100644
--- a/src/pages/TeamDetail/TeamDetail.tsx
+++ b/src/pages/TeamDetail/TeamDetail.tsx
@@ -4,9 +4,18 @@ import styles from './teamDetail.module.scss'
import { DropdownItems, TabProps } from '../../@types/pageTypes'
-import { useCallback, useContext, useEffect, useState } from 'react'
+import { ReactElement, useCallback, useContext, useEffect, useState } from 'react'
import PageLayout from '../../components/PageLayout/PageLayout'
-import { TeamDetailData, getTeamDetail, Team, SharedBuckets, addUserToGroups } from '../../services/teamDetail'
+import {
+ TeamDetailData,
+ getTeamDetail,
+ Team,
+ SharedBuckets,
+ addUserToGroups,
+ removeUserFromGroups,
+ Group,
+ JobResponse,
+} from '../../services/teamDetail'
import { useParams } from 'react-router-dom'
import { ApiError, TokenData, fetchUserInformationFromAuthToken } from '../../utils/services'
@@ -23,12 +32,20 @@ import {
Input,
Dropdown,
Tag,
+ Link,
} from '@statisticsnorway/ssb-component-library'
import PageSkeleton from '../../components/PageSkeleton/PageSkeleton'
import { Skeleton, CircularProgress } from '@mui/material'
import { XCircle } from 'react-feather'
import FormattedTableColumn from '../../components/FormattedTableColumn'
import SidebarModal from '../../components/SidebarModal/SidebarModal'
+import DeleteLink from '../../components/DeleteLink/DeleteLink'
+
+interface UserInfo {
+ name?: string
+ email?: string
+ groups?: Group[]
+}
const TEAM_USERS_TAB = {
title: 'Teammedlemmer',
@@ -73,8 +90,9 @@ const defaultEmail = {
value: '',
}
-const defaultSelectedItem = {
- key: 'add-user-selected-group',
+const defaultAddUserKey = 'add-user-selected-group'
+const defaultEditUserKey = 'edit-user-selected-group'
+const defaultSelectedGroup = {
id: 'velg',
title: 'Velg ...',
}
@@ -93,16 +111,31 @@ const TeamDetail = () => {
)
const [teamDetailTableData, setTeamDetailTableData] = useState()
- const [openSidebar, setOpenSidebar] = useState(false)
+ // Add users to team
+ const [openAddUserSidebarModal, setOpenAddUserSidebarModal] = useState(false)
const [email, setEmail] = useState(defaultEmail)
- const [selectedItem, setSelectedItem] = useState(defaultSelectedItem)
+ const [selectedGroupAddUser, setSelectedGroupAddUser] = useState({
+ ...defaultSelectedGroup,
+ key: defaultAddUserKey,
+ })
const [teamGroupTags, setTeamGroupTags] = useState([])
const [teamGroupTagsError, setTeamGroupTagsError] = useState({
error: false,
errorMessage: 'Velg minst én tilgangsgruppe',
})
const [addUserToTeamErrors, setAddUserToTeamErrors] = useState>([])
- const [showSpinner, setShowSpinner] = useState(false)
+ const [showAddUserSpinner, setShowAddUserSpinner] = useState(false)
+
+ // Edit users in team
+ const [openEditUserSidebarModal, setOpenEditUserSidebarModal] = useState(false)
+ const [editUserInfo, setEditUserInfo] = useState({ name: '', email: '', groups: [] })
+ const [selectedGroupEditUser, setSelectedGroupEditUser] = useState({
+ ...defaultSelectedGroup,
+ key: defaultEditUserKey,
+ })
+ const [userGroupTags, setUserGroupTags] = useState([])
+ const [editUserErrors, setEditUserErrors] = useState>([])
+ const [showEditUserSpinner, setShowEditUserSpinner] = useState(false)
const { teamId } = useParams<{ teamId: string }>()
const teamDetailTab = (activeTab as TabProps)?.path ?? activeTab
@@ -129,8 +162,12 @@ const TeamDetail = () => {
if (!teamUsers) return []
return teamUsers.map(({ display_name, principal_name, section_name, groups }) => {
+ const userFullName = formatDisplayName(display_name)
+ const userGroups = groups?.filter((group) =>
+ group.uniform_name.startsWith((response.team as Team).uniform_name)
+ ) as Group[]
return {
- id: formatDisplayName(display_name),
+ id: userFullName,
navn: (
{
.map((group) => getGroupType(group.uniform_name))
.join(', '),
epost: principal_name,
+ editUser: (
+
+ {
+ setOpenEditUserSidebarModal(true)
+ setEditUserInfo({
+ name: formatDisplayName(display_name),
+ email: principal_name,
+ groups: userGroups,
+ })
+ setUserGroupTags(
+ userGroups.map(({ uniform_name }) => {
+ return { id: uniform_name, title: getGroupType(uniform_name) }
+ })
+ )
+ }}
+ >
+ Endre
+
+
+ ),
}
})
}
@@ -159,6 +217,11 @@ const TeamDetail = () => {
[activeTab]
)
+ const isTeamManager = useCallback(() => {
+ const teamManagers = (teamDetailData && (teamDetailData.team as Team).managers) ?? []
+ return teamManagers?.some((manager) => manager.principal_name === tokenData?.email)
+ }, [tokenData, teamDetailData])
+
useEffect(() => {
if (!teamId) return
fetchUserInformationFromAuthToken()
@@ -179,6 +242,20 @@ const TeamDetail = () => {
})
}, [])
+ useEffect(() => {
+ if (isTeamManager()) {
+ setTeamDetailTableHeaderColumns([
+ ...TEAM_USERS_TAB.columns,
+ {
+ id: 'editUser',
+ label: '',
+ unsortable: true,
+ align: 'center',
+ },
+ ])
+ }
+ }, [isTeamManager])
+
useEffect(() => {
if (teamDetailData) {
if (teamDetailTab === SHARED_BUCKETS_TAB.path) {
@@ -186,7 +263,19 @@ const TeamDetail = () => {
setTeamDetailTableHeaderColumns(SHARED_BUCKETS_TAB.columns)
} else {
setTeamDetailTableTitle(TEAM_USERS_TAB.title)
- setTeamDetailTableHeaderColumns(TEAM_USERS_TAB.columns)
+ if (isTeamManager()) {
+ setTeamDetailTableHeaderColumns([
+ ...TEAM_USERS_TAB.columns,
+ {
+ id: 'editUser',
+ label: '',
+ unsortable: true,
+ align: 'center',
+ },
+ ])
+ } else {
+ setTeamDetailTableHeaderColumns(TEAM_USERS_TAB.columns)
+ }
}
setTeamDetailTableData(prepTeamData(teamDetailData))
}
@@ -241,29 +330,52 @@ const TeamDetail = () => {
}
}
- const handleAddTeamGroupTag = (item: DropdownItems) => {
- const teamGroupsTags = [...teamGroupTags, item].reduce((acc: DropdownItems[], dropdownItem: DropdownItems) => {
+ const removeDuplicateDropdownItems = (items: DropdownItems[]) => {
+ return items.reduce((acc: DropdownItems[], dropdownItem: DropdownItems) => {
const ids = acc.map((obj) => obj.id)
if (!ids.includes(dropdownItem.id)) {
acc.push(dropdownItem)
}
return acc
}, [])
- setTeamGroupTags(teamGroupsTags)
- setTeamGroupTagsError({ ...teamGroupTagsError, error: false })
- setSelectedItem({ ...item, key: `${selectedItem.key}-${item.id}` })
+ }
+
+ const handleAddGroupTag = (item: DropdownItems) => {
+ if (openAddUserSidebarModal) {
+ const teamGroupsTags = removeDuplicateDropdownItems([...teamGroupTags, item])
+ setTeamGroupTags(teamGroupsTags)
+ setTeamGroupTagsError({ ...teamGroupTagsError, error: false })
+ setSelectedGroupAddUser({ ...item, key: `${defaultAddUserKey}-${item.id}` })
+ }
+
+ if (openEditUserSidebarModal) {
+ const userGroupsTagsList = removeDuplicateDropdownItems([...userGroupTags, item])
+ setUserGroupTags(userGroupsTagsList)
+ setSelectedGroupEditUser({ ...item, key: `${defaultEditUserKey}-${item.id}` })
+ }
}
const handleDeleteGroupTag = (item: DropdownItems) => {
- const teamGroupsTags = teamGroupTags.filter((items) => items !== item)
- setTeamGroupTags(teamGroupsTags)
+ if (openAddUserSidebarModal) {
+ const teamGroupsTags = teamGroupTags.filter((items) => items !== item)
+ setTeamGroupTags(teamGroupsTags)
+ }
+
+ if (openEditUserSidebarModal) {
+ const userGroupsTags = userGroupTags.filter((items) => items !== item)
+ setUserGroupTags(userGroupsTags)
+ }
}
- const isUserInputValid = (value?: string) => {
- const regEx = /^[\w-]+@ssb\.no$/
- const userVal = value || email.value
- const testUser = userVal.match(regEx)
- return !!testUser
+ const getErrorList = (response: JobResponse[]) => {
+ return response
+ .map(({ status, detail }) => {
+ if ((detail && status === 'ERROR') || (detail && status === 'IGNORED')) {
+ return detail
+ }
+ return ''
+ })
+ .filter((str) => str !== '')
}
const handleAddUserOnSubmit = () => {
@@ -277,70 +389,181 @@ const TeamDetail = () => {
if (email.value !== '' && teamGroupTags.length) {
setEmail({ ...email, key: `add-user-${email.value}` })
setAddUserToTeamErrors([])
- setShowSpinner(true)
+ setShowAddUserSpinner(true)
addUserToGroups(
teamGroupTags.map((group) => group.id),
email.value
)
.then((response) => {
- const errorsList = response
- .map(({ status, detail }) => {
- if ((detail && status === 'ERROR') || (detail && status === 'IGNORED')) {
- return detail
- }
- return ''
- })
- .filter((str) => str !== '')
-
+ const errorsList = getErrorList(response)
if (errorsList.length) {
setAddUserToTeamErrors(errorsList)
} else {
- setOpenSidebar(false)
+ setOpenAddUserSidebarModal(false)
setTeamGroupTags([])
// Reset fields with their respective keys; re-initializes component
setEmail({ ...defaultEmail })
- setSelectedItem({ ...defaultSelectedItem })
+ setSelectedGroupAddUser({ ...defaultSelectedGroup, key: defaultAddUserKey })
}
})
.catch((e) => setAddUserToTeamErrors(e.message))
- .finally(() => setShowSpinner(false))
+ .finally(() => setShowAddUserSpinner(false))
+ }
+ }
+
+ const handleEditUserOnSubmit = () => {
+ const addedGroups =
+ userGroupTags?.filter((groupTag) => !editUserInfo.groups?.some((group) => groupTag.id === group.uniform_name)) ??
+ []
+ const removedGroups =
+ editUserInfo.groups?.filter((group) => !userGroupTags?.some((groupTag) => groupTag.id === group.uniform_name)) ??
+ []
+ if (addedGroups.length && removedGroups.length) {
+ setEditUserErrors([])
+ setShowEditUserSpinner(true)
+ Promise.all([
+ addUserToGroups(
+ addedGroups.map((group) => group.id),
+ editUserInfo?.email as string
+ ),
+ removeUserFromGroups(
+ removedGroups.map((group) => group.uniform_name),
+ editUserInfo?.email as string
+ ),
+ ])
+ .then((response) => {
+ const flattenedResponse = [...response[0], ...response[1]]
+ const errorsList = getErrorList(flattenedResponse)
+ if (errorsList.length) {
+ setEditUserErrors(errorsList)
+ } else {
+ setOpenEditUserSidebarModal(false)
+ // Reset fields with their respective keys; re-initializes component
+ setSelectedGroupEditUser({ ...defaultSelectedGroup, key: defaultEditUserKey })
+ }
+ })
+ .catch((e) => setEditUserErrors(e.message))
+ .finally(() => setShowEditUserSpinner(false))
+ return
+ }
+
+ if (removedGroups.length) {
+ setEditUserErrors([])
+ setShowEditUserSpinner(true)
+ removeUserFromGroups(
+ removedGroups?.map((group) => group.uniform_name),
+ editUserInfo.email as string
+ )
+ .then((response) => {
+ const errorsList = getErrorList(response)
+ if (errorsList.length) {
+ setEditUserErrors(errorsList)
+ } else {
+ setOpenEditUserSidebarModal(false)
+ // Reset fields with their respective keys; re-initializes component
+ setSelectedGroupEditUser({ ...defaultSelectedGroup, key: defaultEditUserKey })
+ }
+ })
+ .catch((e) => setEditUserErrors(e.message))
+ .finally(() => setShowEditUserSpinner(false))
+ return
+ }
+
+ if (addedGroups.length) {
+ setEditUserErrors([])
+ setShowEditUserSpinner(true)
+ addUserToGroups(
+ addedGroups.map((group) => group.id),
+ editUserInfo?.email as string
+ )
+ .then((response) => {
+ const errorsList = getErrorList(response)
+ if (errorsList.length) {
+ setEditUserErrors(errorsList)
+ } else {
+ setOpenEditUserSidebarModal(false)
+ // Reset fields with their respective keys; re-initializes component
+ setSelectedGroupEditUser({ ...defaultSelectedGroup, key: defaultEditUserKey })
+ }
+ })
+ .catch((e) => setEditUserErrors(e.message))
+ .finally(() => setShowEditUserSpinner(false))
+ return
+ }
+ }
+
+ const handleDeleteUser = () => {
+ if (editUserInfo.groups && editUserInfo.groups.length) {
+ setEditUserErrors([])
+ setShowEditUserSpinner(true)
+ removeUserFromGroups(
+ editUserInfo.groups.map(({ uniform_name }) => uniform_name),
+ editUserInfo.email as string
+ )
+ .then((response) => {
+ const errorsList = getErrorList(response)
+ if (errorsList.length) {
+ setEditUserErrors(errorsList)
+ } else {
+ setOpenEditUserSidebarModal(false)
+ // Reset fields with their respective keys; re-initializes component
+ setSelectedGroupEditUser({ ...defaultSelectedGroup, key: defaultEditUserKey })
+ }
+ })
+ .catch((e) => setEditUserErrors(e.message))
+ .finally(() => setShowEditUserSpinner(false))
}
}
- const renderSidebarModalAlert = () => {
+ const renderSidebarModalInfo = (children: ReactElement) => {
return (
- {addUserToTeamErrors.length ? (
-
- ) : null}
- {showSpinner &&
}
+ {children}
)
}
- const renderSidebarModal = () => {
+ const renderSidebarModalWarning = (errorList: string[]) => {
+ return (
+
+ )
+ }
+
+ const isUserInputValid = (value?: string) => {
+ const regEx = /^[\w-]+@ssb\.no$/
+ const userVal = value || email.value
+ const testUser = userVal.match(regEx)
+ return !!testUser
+ }
+
+ const teamModalHeader = teamDetailData
+ ? {
+ modalType: 'Medlem',
+ modalTitle: `${(teamDetailData?.team as Team).display_name}`,
+ modalDescription: `${(teamDetailData?.team as Team).uniform_name}`,
+ }
+ : {
+ modalTitle: '',
+ }
+ const teamGroups = teamDetailData ? ((teamDetailData.team as Team).groups as Group[]) : []
+ const renderAddUserSidebarModal = () => {
if (teamDetailData) {
- const teamGroups = (teamDetailData?.team as Team).groups ?? []
return (
setOpenSidebar(false)}
- header={{
- modalType: 'Medlem',
- modalTitle: `${(teamDetailData?.team as Team).display_name}`,
- modalDescription: `${(teamDetailData?.team as Team).uniform_name}`,
- }}
+ open={openAddUserSidebarModal}
+ onClose={() => setOpenAddUserSidebarModal(false)}
+ header={teamModalHeader}
footer={{
submitButtonText: 'Legg til medlem',
handleSubmit: handleAddUserOnSubmit,
@@ -371,15 +594,15 @@ const TeamDetail = () => {
}
/>
({
id: uniform_name,
title: getGroupType(uniform_name),
}))}
- onSelect={handleAddTeamGroupTag}
+ onSelect={(item: DropdownItems) => handleAddGroupTag(item)}
error={teamGroupTagsError.error}
errorMessage={teamGroupTagsError.errorMessage}
/>
@@ -395,7 +618,71 @@ const TeamDetail = () => {
))}
- {renderSidebarModalAlert()}
+
+ {renderSidebarModalInfo(
+ <>
+ {addUserToTeamErrors.length ? renderSidebarModalWarning(addUserToTeamErrors) : null}
+ {showAddUserSpinner && }
+ >
+ )}
+
+ >
+ ),
+ }}
+ />
+ )
+ }
+ }
+
+ const renderEditUserSidebarModal = () => {
+ if (teamDetailData && editUserInfo) {
+ return (
+ setOpenEditUserSidebarModal(false)}
+ header={teamModalHeader}
+ footer={{
+ submitButtonText: 'Oppdater Tilgang',
+ handleSubmit: handleEditUserOnSubmit,
+ }}
+ body={{
+ modalBodyTitle: `Endre tilgang til "${editUserInfo.name}"`,
+ modalBody: (
+ <>
+ ({
+ id: uniform_name,
+ title: getGroupType(uniform_name),
+ }))}
+ onSelect={(item: DropdownItems) => handleAddGroupTag(item)}
+ />
+
+ {userGroupTags &&
+ userGroupTags.map((group) => (
+ }
+ onClick={() => handleDeleteGroupTag(group)}
+ >
+ {group.title}
+
+ ))}
+
+
+
+ Fjern fra teamet
+
+ {renderSidebarModalInfo(
+ <>
+ {editUserErrors.length ? renderSidebarModalWarning(editUserErrors) : null}
+ {showEditUserSpinner && }
+ >
+ )}
+
>
),
}}
@@ -404,10 +691,10 @@ const TeamDetail = () => {
}
}
- const teamManager = teamDetailData ? (teamDetailData?.team as Team).managers : []
return (
<>
- {renderSidebarModal()}
+ {renderAddUserSidebarModal()}
+ {renderEditUserSidebarModal()}
{
}
content={renderContent()}
button={
- teamManager?.some((manager) => manager.principal_name === tokenData?.email) ? (
-
- ) : undefined
+ isTeamManager() ? : undefined
}
/>
>
diff --git a/src/services/teamDetail.ts b/src/services/teamDetail.ts
index d11e6518..5450809a 100644
--- a/src/services/teamDetail.ts
+++ b/src/services/teamDetail.ts
@@ -34,7 +34,7 @@ export interface User {
groups?: Group[]
}
-interface Group {
+export interface Group {
uniform_name: string
display_name: string
}
@@ -62,6 +62,8 @@ export interface JobResponse {
detail?: string
}
+type Method = 'POST' | 'DELETE' // POST = ADD, DELETE = REMOVE
+
export const fetchTeamInfo = async (teamId: string): Promise => {
const teamsUrl = new URL(`${TEAMS_URL}/${teamId}`, window.location.origin)
const embeds = ['users', 'users.groups', 'managers', 'groups']
@@ -196,7 +198,9 @@ export const getTeamDetail = async (teamId: string): Promise =>
export const addUserToGroups = async (groupIds: string[], userPrincipalName: string): Promise => {
try {
- const jobResponses = await Promise.all(groupIds.map((groupId) => addUserToGroup(groupId, userPrincipalName)))
+ const jobResponses = await Promise.all(
+ groupIds.map((groupId) => updateGroupMembership(groupId, userPrincipalName, 'POST'))
+ )
return jobResponses
} catch (error) {
if (error instanceof ApiError) {
@@ -210,18 +214,50 @@ export const addUserToGroups = async (groupIds: string[], userPrincipalName: str
}
}
-const addUserToGroup = async (groupId: string, userPrincipalName: string): Promise => {
- const groupsUrl = `${GROUPS_URL}/${groupId}/users`
+export const removeUserFromGroups = async (groupIds: string[], userPrincipalName: string): Promise => {
try {
- const response = await fetch(groupsUrl, {
- method: 'POST',
- headers: {
- 'content-type': 'application/json',
- },
- body: JSON.stringify({
- users: [userPrincipalName],
- }),
- })
+ const jobResponses = await Promise.all(
+ groupIds.map((groupId) => updateGroupMembership(groupId, userPrincipalName, 'DELETE'))
+ )
+ return jobResponses
+ } catch (error) {
+ if (error instanceof ApiError) {
+ console.error('Failed to remove user from groups: ', error)
+ throw error
+ } else {
+ const apiError = new ApiError(500, 'An unexpected error occurred')
+ console.error('Failed to remove user from groups: ', apiError)
+ throw apiError
+ }
+ }
+}
+
+const updateGroupMembership = async (
+ groupId: string,
+ userPrincipalName: string,
+ method: Method
+): Promise => {
+ let groupsUrl = `${GROUPS_URL}/${groupId}/users`
+ const fetchOptions: RequestInit = {
+ method: method,
+ headers: {
+ Accept: '*/*',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ users: [userPrincipalName],
+ }),
+ }
+
+ // TODO: Remove me once DELETE with proxy is fixed
+ if (method === 'DELETE') {
+ groupsUrl = `/localApi/groups/${groupId}/${userPrincipalName}`
+ // Don't include body in fetch options for DELETE method
+ delete fetchOptions.body
+ }
+
+ try {
+ const response = await fetch(groupsUrl, fetchOptions)
if (!response.ok) {
const errorMessage = (await response.text()) || 'An error occurred'
@@ -230,16 +266,16 @@ const addUserToGroup = async (groupId: string, userPrincipalName: string): Promi
}
const responseJson = await response.json()
- const flattendResponse = { ...responseJson._embedded.results[0] }
+ const flattenedResponse = { ...responseJson._embedded.results[0] }
- return flattendResponse
+ return flattenedResponse
} catch (error) {
if (error instanceof ApiError) {
- console.error('Failed to add user to group: ', error)
+ console.error('Failed to update group membership: ', error)
throw error
} else {
const apiError = new ApiError(500, 'An unexpected error occurred')
- console.error('Failed to add user to group: ', apiError)
+ console.error('Failed to update group membership: ', apiError)
throw apiError
}
}