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 ( + + {`${error?.code} - ${error?.message}`} + + ) + } + + 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: +
+ ) : ( + + You are not a manager in any dapla-team + + )} + + ) + } + } + + 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 + } +}