diff --git a/server.js b/server.js
index 652e008c..a0af2cb7 100644
--- a/server.js
+++ b/server.js
@@ -1,12 +1,14 @@
import ViteExpress from 'vite-express'
import { createLightship } from 'lightship'
-import cache from 'memory-cache'
import express from 'express'
import jwt from 'jsonwebtoken'
import jwksClient from 'jwks-rsa'
import { getReasonPhrase } from 'http-status-codes'
import dotenv from 'dotenv'
+// TODO: Do a massive cleanup. There are much of the code that can be re-written for reuseability, and some functions
+// may not even be required anymore after dapla-team-api-redux changes.
+
if (!process.env.VITE_JWKS_URI) {
dotenv.config({ path: './.env.local' })
}
@@ -141,22 +143,13 @@ app.get('/api/userProfile/:principalName', tokenVerificationMiddleware, async (r
const userManagerUrl = `${DAPLA_TEAM_API_URL}/users/${principalName}/manager`
const userPhotoUrl = `${DAPLA_TEAM_API_URL}/users/${principalName}/photo`
- const cacheKey = `userProfile-${principalName}`
- const cachedUserProfile = cache.get(cacheKey)
- if (cachedUserProfile) {
- return res.json(cachedUserProfile)
- }
-
const [userProfile, userManager, userPhoto] = await Promise.all([
fetchAPIData(token, userProfileUrl, 'Failed to fetch userProfile'),
fetchAPIData(token, userManagerUrl, 'Failed to fetch user manager').catch(() => managerFallback()),
fetchPhoto(token, userPhotoUrl, 'Failed to fetch user photo'),
])
- const data = { ...userProfile, manager: { ...userManager }, photo: userPhoto }
- cache.put(cacheKey, data, 3600000)
-
- return res.json(data)
+ return res.json({ ...userProfile, manager: { ...userManager }, photo: userPhoto })
} catch (error) {
next(error)
}
@@ -254,7 +247,7 @@ app.get('/api/teamDetail/:teamUniformName', tokenVerificationMiddleware, async (
const [teamInfo, teamUsers] = await Promise.all([
fetchAPIData(token, teamInfoUrl, 'Failed to fetch team info').then(async (teamInfo) => {
- const manager = await fetchTeamManager(token, teamInfo)
+ const manager = await fetchTeamManager(token, teamInfo.uniform_name)
return { ...teamInfo, manager }
}),
fetchAPIData(token, teamUsersUrl, 'Failed to fetch team users').then(async (teamUsers) => {
@@ -270,8 +263,8 @@ app.get('/api/teamDetail/:teamUniformName', tokenVerificationMiddleware, async (
}
})
-async function fetchTeamManager(token, teamInfo) {
- const teamManagerUrl = `${DAPLA_TEAM_API_URL}/groups/${teamInfo.uniform_name}-managers/users`
+async function fetchTeamManager(token, teamUniformName) {
+ const teamManagerUrl = `${DAPLA_TEAM_API_URL}/groups/${teamUniformName}-managers/users`
return await fetchAPIData(token, teamManagerUrl, 'Failed to fetch team manager')
.then((teamManager) => {
return teamManager.count > 0 ? teamManager._embedded.users[0] : managerFallback()
diff --git a/src/App.tsx b/src/App.tsx
index 8eb527ec..4b1f3779 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,6 +5,7 @@ import Login from './pages/Login/Login'
import TeamOverview from './pages/TeamOverview/TeamOverview'
import UserProfile from './pages/UserProfile/UserProfile'
import TeamDetail from './pages/TeamDetail/TeamDetail'
+import TeamMembers from './pages/TeamMembers/TeamMembers'
import { Routes, Route } from 'react-router-dom'
@@ -17,7 +18,7 @@ export default function App() {
example:
*/}
} />
- Teammedlemmer} />
+ } />
} />
} />
} />
diff --git a/src/pages/TeamMembers/TeamMembers.tsx b/src/pages/TeamMembers/TeamMembers.tsx
new file mode 100644
index 00000000..b3ab12d4
--- /dev/null
+++ b/src/pages/TeamMembers/TeamMembers.tsx
@@ -0,0 +1,153 @@
+import pageLayoutStyles from '../../components/PageLayout/pagelayout.module.scss'
+
+import { useCallback, useEffect, useState } from 'react'
+import { Dialog, Title, Text, Link, Tabs, Divider } from '@statisticsnorway/ssb-component-library'
+
+import { TabProps } from '../../@types/pageTypes'
+import PageLayout from '../../components/PageLayout/PageLayout'
+import Table, { TableData } from '../../components/Table/Table'
+import PageSkeleton from '../../components/PageSkeleton/PageSkeleton'
+
+import { fetchAllTeamMembersData, TeamMembersData, User } from '../../services/teamMembers'
+import { formatDisplayName } from '../../utils/utils'
+import { ApiError } from '../../utils/services'
+
+export default function TeamMembers() {
+ const accessToken = localStorage.getItem('access_token') || ''
+ const jwt = JSON.parse(atob(accessToken.split('.')[1]))
+
+ const defaultActiveTab = {
+ title: 'Mine teammedlemmer',
+ path: 'myUsers',
+ }
+
+ const [activeTab, setActiveTab] = useState(defaultActiveTab)
+ const [teamMembersData, setTeamMembersData] = useState()
+ const [teamMembersTableData, setTeamMembersTableData] = useState()
+ const [teamMembersTableTitle, setTeamMembersTableTitle] = useState(defaultActiveTab.title)
+ const [error, setError] = useState()
+ const [loading, setLoading] = useState(true)
+
+ const prepUserData = useCallback(
+ (response: TeamMembersData): TableData['data'] => {
+ const teamMember = (activeTab as TabProps)?.path ?? activeTab
+
+ return response[teamMember].users.map((teamMember) => ({
+ id: teamMember.principal_name,
+ navn: renderUserNameColumn(teamMember),
+ team: teamMember.teams.length,
+ data_admin_roller: teamMember.groups.filter((group) => group.uniform_name.endsWith('data-admins')).length,
+ seksjon: teamMember.section_name,
+ seksjonsleder: formatDisplayName(
+ teamMember.section_manager && teamMember.section_manager.length > 0
+ ? teamMember.section_manager[0].display_name
+ : 'Seksjonsleder ikke funnet'
+ ),
+ }))
+ },
+ [activeTab]
+ )
+
+ useEffect(() => {
+ if (!jwt) return
+ fetchAllTeamMembersData(jwt.email)
+ .then((response) => {
+ setTeamMembersData(response as TeamMembersData)
+ setTeamMembersTableData(prepUserData(response as TeamMembersData))
+ })
+ .finally(() => setLoading(false))
+ .catch((error) => {
+ setError(error as ApiError)
+ })
+ }, [prepUserData, jwt])
+
+ useEffect(() => {
+ if (teamMembersData) {
+ setTeamMembersTableData(prepUserData(teamMembersData))
+ }
+ }, [teamMembersData, prepUserData])
+
+ const handleTabClick = (tab: string) => {
+ setActiveTab(tab)
+ if (tab === 'myUsers') {
+ setTeamMembersTableTitle('Mine teammedlemmer')
+ } else {
+ setTeamMembersTableTitle('Alle teammedlemmer')
+ }
+ }
+
+ function renderUserNameColumn(user: User) {
+ return (
+ <>
+
+
+ {user.display_name}
+
+
+ {user.section_name && {user.section_name}}
+ >
+ )
+ }
+
+ function renderErrorAlert() {
+ return (
+
+ )
+ }
+
+ function renderContent() {
+ if (error) return renderErrorAlert()
+ if (loading) return
+
+ if (teamMembersTableData) {
+ const teamMembersTableHeaderColumns = [
+ {
+ id: 'navn',
+ label: 'Navn',
+ },
+ {
+ id: 'team',
+ label: 'Team',
+ },
+ {
+ id: 'data_admin_roller',
+ label: 'Data-admin-roller',
+ },
+ {
+ id: 'seksjonsleder',
+ label: 'Seksjonsleder',
+ },
+ ]
+
+ return (
+ <>
+
+
+ {/* TODO: Remove Title */}
+
+ {teamMembersTableTitle}
+
+ {teamMembersTableData.length > 0 ? (
+ //TODO:
+
+ ) : (
+
+ )}
+ >
+ )
+ }
+ }
+
+ return
+}
diff --git a/src/pages/TeamMembers/teamMembers.module.scss b/src/pages/TeamMembers/teamMembers.module.scss
new file mode 100644
index 00000000..24cbd24e
--- /dev/null
+++ b/src/pages/TeamMembers/teamMembers.module.scss
@@ -0,0 +1 @@
+@use '@statisticsnorway/ssb-component-library/src/style/variables' as variables;
\ No newline at end of file
diff --git a/src/services/teamMembers.ts b/src/services/teamMembers.ts
new file mode 100644
index 00000000..e461ae92
--- /dev/null
+++ b/src/services/teamMembers.ts
@@ -0,0 +1,184 @@
+import { ApiError, fetchAPIData } from '../utils/services'
+
+const DAPLA_TEAM_API_URL = import.meta.env.VITE_DAPLA_TEAM_API_URL
+const USERS_URL = `${DAPLA_TEAM_API_URL}/users`
+
+export interface TeamMembersData {
+ [key: string]: UsersData // myUsers, allUsers
+}
+
+interface UsersData {
+ users: User[]
+}
+
+export interface User {
+ principal_name: string
+ display_name: string
+ section_name: string
+ section_manager: sectionManager[]
+ teams: Team[]
+ groups: Group[]
+ // eslint-disable-next-line
+ _embedded?: any
+}
+
+interface sectionManager {
+ display_name: string
+ principal_name: string
+}
+
+interface Team {
+ uniform_name: string
+}
+
+interface Group {
+ uniform_name: string
+}
+
+const fetchManagedUsers = async (accessToken: string, principalName: string): Promise => {
+ const usersUrl = new URL(`${USERS_URL}/${principalName}`)
+ const embeds = ['managed_users']
+ const selects = ['managed_users.principal_name']
+
+ usersUrl.searchParams.set('embed', embeds.join(','))
+ usersUrl.searchParams.append('select', selects.join(','))
+
+ try {
+ const managedUsersData = await fetchAPIData(usersUrl.toString(), accessToken)
+
+ if (!managedUsersData) throw new ApiError(500, 'No json data returned')
+ if (!managedUsersData._embedded || !managedUsersData._embedded.managed_users) return [] // return an empty list if the user does not have any managed_users
+
+ return managedUsersData._embedded.managed_users
+ } catch (error) {
+ if (error instanceof ApiError) {
+ console.error('Failed to fetch managed users:', error)
+ throw error
+ } else {
+ const apiError = new ApiError(500, 'An unexpected error occurred')
+ console.error('Failed to fetch managed users:', apiError)
+ throw apiError
+ }
+ }
+}
+
+export const fetchManagedUsersManagers = async (accessToken: string, principalName: string): Promise => {
+ try {
+ const users = await fetchManagedUsers(accessToken, principalName)
+
+ const prepUsers = await Promise.all(
+ users.map(async (user): Promise => {
+ const usersUrl = new URL(`${USERS_URL}/${user.principal_name}`)
+ const embeds = ['teams', 'groups', 'section_manager']
+
+ const selects = [
+ 'principal_name',
+ 'display_name',
+ 'section_name',
+ 'teams.uniform_name',
+ 'groups.uniform_name',
+ 'section_manager.display_name',
+ 'section_manager.principal_name',
+ ]
+
+ usersUrl.searchParams.set('embed', embeds.join(','))
+ usersUrl.searchParams.append('select', selects.join(','))
+
+ const managedUsersData = await fetchAPIData(usersUrl.toString(), accessToken)
+
+ const prepData = {
+ ...managedUsersData,
+ ...managedUsersData._embedded,
+ }
+ delete prepData._embedded
+
+ return prepData
+ })
+ )
+
+ return { users: prepUsers }
+ } catch (error) {
+ if (error instanceof ApiError) {
+ console.error('Failed to fetch managed users:', error)
+ throw error
+ } else {
+ const apiError = new ApiError(500, 'An unexpected error occurred')
+ console.error('Failed to fetch managed users:', apiError)
+ throw apiError
+ }
+ }
+}
+
+export const fetchAllUsers = async (accessToken: string): Promise => {
+ const usersUrl = new URL(`${USERS_URL}`)
+ const embeds = ['section_manager', 'teams', 'groups']
+
+ const selects = [
+ 'display_name',
+ 'principal_name',
+ 'section_name',
+ 'section_manager.display_name',
+ 'section_manager.principal_name',
+ 'teams.uniform_name',
+ 'groups.uniform_name',
+ ]
+
+ usersUrl.searchParams.set('embed', embeds.join(','))
+ usersUrl.searchParams.append('select', selects.join(','))
+
+ try {
+ const allUsersData = await fetchAPIData(usersUrl.toString(), accessToken)
+
+ if (!allUsersData) throw new ApiError(500, 'No json data returned')
+ if (!allUsersData._embedded || !allUsersData._embedded.users) throw new ApiError(500, 'Did not receive users data')
+
+ const prepData = allUsersData._embedded.users.map((user: User) => {
+ const prepUserData = {
+ ...user,
+ ...user._embedded,
+ }
+ delete prepUserData._embedded
+ return prepUserData
+ })
+ delete prepData._embedded
+
+ return { users: prepData }
+ } catch (error) {
+ if (error instanceof ApiError) {
+ console.error('Failed to fetch all users:', error)
+ throw error
+ } else {
+ const apiError = new ApiError(500, 'An unexpected error occurred')
+ console.error('Failed to fetch all users:', apiError)
+ throw apiError
+ }
+ }
+}
+
+export const fetchAllTeamMembersData = async (principalName: string): Promise => {
+ const accessToken = localStorage.getItem('access_token')
+ if (!accessToken) {
+ console.error('No access token available')
+ const apiError = new ApiError(401, 'No access token available')
+ console.error('Failed to fetch team members data:', apiError)
+ throw apiError
+ }
+
+ try {
+ const [myUsers, allUsers] = await Promise.all([
+ fetchManagedUsersManagers(accessToken, principalName),
+ fetchAllUsers(accessToken),
+ ])
+
+ return { myUsers: myUsers, allUsers: allUsers } as TeamMembersData
+ } catch (error) {
+ if (error instanceof ApiError) {
+ console.error('Failed to fetch team members data:', error)
+ throw error
+ } else {
+ const apiError = new ApiError(500, 'An unexpected error occurred while fetching team members data')
+ console.error('Failed to fetch team members data:', apiError)
+ throw apiError
+ }
+ }
+}
diff --git a/src/utils/services.ts b/src/utils/services.ts
new file mode 100644
index 00000000..d927de0e
--- /dev/null
+++ b/src/utils/services.ts
@@ -0,0 +1,28 @@
+// eslint-disable-next-line
+export const fetchAPIData = async (url: string, accessToken: string): Promise => {
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ accept: '*/*',
+ Authorization: `Bearer ${accessToken}`,
+ },
+ })
+
+ if (!response.ok) {
+ const errorMessage = (await response.text()) || 'An error occurred'
+ const { detail, status } = JSON.parse(errorMessage)
+ throw new ApiError(status, detail)
+ }
+
+ return response.json()
+}
+
+export class ApiError extends Error {
+ public code: number
+
+ constructor(code: number, message: string) {
+ super(message)
+ this.name = 'ApiError'
+ this.code = code
+ }
+}