From 60bf51622e38f065bb80cea6c4c46783c2e778c8 Mon Sep 17 00:00:00 2001 From: Johnny Niklasson Date: Mon, 4 Mar 2024 09:50:44 +0100 Subject: [PATCH] Refactor to embeds (#72) Refactored all endpoints to use embedding from dapla-team-api. This PR needs to merge to be able to take the next step, which is to add bucket data and add team members. There will be a new branch in the future to clean up. --- server.js | 252 +----------------------- src/@types/error.ts | 8 - src/@types/group.ts | 7 - src/@types/team.ts | 12 -- src/@types/user.ts | 5 +- src/components/Avatar/Avatar.tsx | 4 +- src/pages/TeamDetail/TeamDetail.tsx | 63 +++--- src/pages/TeamOverview/TeamOverview.tsx | 37 ++-- src/pages/UserProfile/UserProfile.tsx | 77 +++----- src/services/teamDetail.ts | 121 +++++++++--- src/services/teamOverview.ts | 187 ++++++++++++++++-- src/services/userProfile.ts | 199 ++++++++++++++----- src/utils/utils.ts | 18 ++ 13 files changed, 516 insertions(+), 474 deletions(-) delete mode 100644 src/@types/error.ts delete mode 100644 src/@types/group.ts delete mode 100644 src/@types/team.ts diff --git a/server.js b/server.js index a0af2cb7..4d381ba0 100644 --- a/server.js +++ b/server.js @@ -6,9 +6,6 @@ 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' }) } @@ -79,77 +76,16 @@ app.post('/api/verify-token', (req, res) => { }) }) -app.get('/api/teamOverview', tokenVerificationMiddleware, async (req, res, next) => { - const token = req.token - const principalName = req.user.email - const allteamsUrl = `${DAPLA_TEAM_API_URL}/teams` - const myTeamsUrl = `${DAPLA_TEAM_API_URL}/users/${principalName}/teams` - - try { - const [allTeams, myTeams] = await Promise.all([ - fetchAPIData(token, allteamsUrl, 'Failed to fetch all teams').then((teams) => getTeamOverviewTeams(token, teams)), - fetchAPIData(token, myTeamsUrl, 'Failed to fetch my teams').then((teams) => getTeamOverviewTeams(token, teams)), - ]) - - const result = { - allTeams: { - count: allTeams.count, - ...allTeams._embedded, - }, - myTeams: { - count: myTeams.count, - ...myTeams._embedded, - }, - } - - res.json(result) - } catch (error) { - next(error) - } -}) - -async function getTeamOverviewTeams(token, teams) { - const teamPromises = teams._embedded.teams.map(async (team) => { - const teamUniformName = team.uniform_name - const teamInfoUrl = `${DAPLA_TEAM_API_URL}/teams/${teamUniformName}` - const teamUsersUrl = `${DAPLA_TEAM_API_URL}/teams/${teamUniformName}/users` - const teamManagerUrl = `${DAPLA_TEAM_API_URL}/groups/${teamUniformName}-managers/users` - - const [teamInfo, teamUsers, teamManager] = await Promise.all([ - fetchAPIData(token, teamInfoUrl, 'Failed to fetch team info').catch(() => sectionFallback(teamUniformName)), - fetchAPIData(token, teamUsersUrl, 'Failed to fetch team users'), - fetchAPIData(token, teamManagerUrl, 'Failed to fetch team manager').catch(() => managerFallback()), - ]) - team['section_name'] = teamInfo.section_name - team['team_user_count'] = teamUsers.count - team['manager'] = teamManager.count > 0 ? teamManager._embedded.users[0] : managerFallback() - - return { ...team } - }) - - const resolvedTeams = await Promise.all(teamPromises) - const validTeams = resolvedTeams.filter((team) => team !== null) - - teams._embedded.teams = validTeams - teams.count = validTeams.length - return teams -} +// DO NOT REMOVE, NECCESSARY FOR FRONTEND +app.get('/api/photo/:principalName', tokenVerificationMiddleware, async (req, res, next) => { + const accessToken = req.token + const principalName = req.params.principalName + const userPhotoUrl = `${DAPLA_TEAM_API_URL}/users/${principalName}/photo` -app.get('/api/userProfile/:principalName', tokenVerificationMiddleware, async (req, res, next) => { try { - const token = req.token - const principalName = req.params.principalName - const userProfileUrl = `${DAPLA_TEAM_API_URL}/users/${principalName}` - const userManagerUrl = `${DAPLA_TEAM_API_URL}/users/${principalName}/manager` - const userPhotoUrl = `${DAPLA_TEAM_API_URL}/users/${principalName}/photo` - - 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 photoData = await fetchPhoto(accessToken, userPhotoUrl) - return res.json({ ...userProfile, manager: { ...userManager }, photo: userPhoto }) + return res.send({ photo: photoData }) } catch (error) { next(error) } @@ -167,162 +103,6 @@ async function fetchPhoto(token, url, fallbackErrorMessage) { return photoBuffer.toString('base64') } -async function getUserProfileTeamData(token, principalName, teams) { - const teamPromises = teams._embedded.teams.map(async (team) => { - const teamUniformName = team.uniform_name - const teamInfoUrl = `${DAPLA_TEAM_API_URL}/teams/${teamUniformName}` - const teamGroupsUrl = `${DAPLA_TEAM_API_URL}/teams/${teamUniformName}/groups` - const teamManagerUrl = `${DAPLA_TEAM_API_URL}/groups/${teamUniformName}-managers/users` - - const [teamInfo, teamGroups, teamManager] = await Promise.all([ - fetchAPIData(token, teamInfoUrl, 'Failed to fetch team info').catch(() => sectionFallback(teamUniformName)), - fetchAPIData(token, teamGroupsUrl, 'Failed to fetch groups').then((response) => { - const groupPromises = response._embedded.groups.map((group) => fetchUserGroups(group, token, principalName)) - return Promise.all(groupPromises).then((groupsArrays) => groupsArrays.flat()) - }), - fetchAPIData(token, teamManagerUrl, 'Failed to fetch team manager').catch(() => managerFallback()), - ]) - - team['section_name'] = teamInfo.section_name - team['manager'] = teamManager.count > 0 ? teamManager._embedded.users[0] : managerFallback() - team['groups'] = teamGroups - - return { ...team } - }) - - const resolvedTeams = await Promise.all(teamPromises) - const validTeams = resolvedTeams.filter((team) => team !== null) - - teams._embedded.teams = validTeams - teams.count = validTeams.length - return teams -} - -async function fetchUserGroups(group, token, principalName) { - const groupUsersUrl = `${DAPLA_TEAM_API_URL}/groups/${group.uniform_name}/users` - try { - const groupUsers = await fetchAPIData(token, groupUsersUrl, 'Failed to fetch group users') - if (!groupUsers._embedded || !groupUsers._embedded.users || groupUsers._embedded.users.length === 0) { - return [] - } - - return groupUsers._embedded.users - .filter((user) => user.principal_name === principalName) - .map(() => group.uniform_name) - } catch (error) { - console.error(`Error processing group ${group.uniform_name}:`, error) - throw error - } -} - -app.get('/api/userProfile/:principalName/team', tokenVerificationMiddleware, async (req, res, next) => { - const token = req.token - const principalName = req.params.principalName - const myTeamsUrl = `${DAPLA_TEAM_API_URL}/users/${principalName}/teams` - - try { - const [myTeams] = await Promise.all([ - fetchAPIData(token, myTeamsUrl, 'Failed to fetch my teams').then((teams) => - getUserProfileTeamData(token, principalName, teams) - ), - ]) - - const result = { - count: myTeams.count, - ...myTeams._embedded, - } - - res.json(result) - } catch (error) { - next(error) - } -}) - -app.get('/api/teamDetail/:teamUniformName', tokenVerificationMiddleware, async (req, res, next) => { - try { - const token = req.token - const teamUniformName = req.params.teamUniformName - const teamInfoUrl = `${DAPLA_TEAM_API_URL}/teams/${teamUniformName}` - const teamUsersUrl = `${DAPLA_TEAM_API_URL}/teams/${teamUniformName}/users` - - const [teamInfo, teamUsers] = await Promise.all([ - fetchAPIData(token, teamInfoUrl, 'Failed to fetch team info').then(async (teamInfo) => { - const manager = await fetchTeamManager(token, teamInfo.uniform_name) - return { ...teamInfo, manager } - }), - fetchAPIData(token, teamUsersUrl, 'Failed to fetch team users').then(async (teamUsers) => { - const resolvedUsers = await fetchTeamUsersWithGroups(token, teamUsers, teamUniformName) - return { ...teamUsers, _embedded: { users: resolvedUsers } } - }), - ]) - - // TODO: Implement shared data tab - res.json({ teamUsers: { teamInfo, teamUsers: teamUsers._embedded.users } }) - } catch (error) { - next(error) - } -}) - -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() - }) - .catch(() => managerFallback()) -} - -async function fetchTeamUsersWithGroups(token, teamUsers, teamUniformName) { - const userPromises = teamUsers._embedded.users.map(async (user) => { - const userUrl = `${DAPLA_TEAM_API_URL}/users/${user.principal_name}` - const userGroupsUrl = `${DAPLA_TEAM_API_URL}/users/${user.principal_name}/groups` - const currentUser = await fetchAPIData(token, userUrl, 'Failed to fetch user') - const groups = await fetchAPIData(token, userGroupsUrl, 'Failed to fetch groups').catch(() => groupFallback()) - - const flattenedGroups = groups._embedded.groups - .filter((group) => group !== null && group.uniform_name.startsWith(teamUniformName)) - .flatMap((group) => group) - - currentUser.groups = flattenedGroups - - return { ...currentUser } - }) - return await Promise.all(userPromises) -} - -async function fetchAPIData(token, url, fallbackErrorMessage) { - const response = await fetch(url, getFetchOptions(token)) - const wwwAuthenticate = response.headers.get('www-authenticate') - - if (!response.ok) { - const { error_description } = wwwAuthenticate - ? parseWwwAuthenticate(wwwAuthenticate) - : { error_description: fallbackErrorMessage } - throw new APIError(error_description, response.status) - } - - return response.json() -} - -function parseWwwAuthenticate(header) { - const parts = header.split(',') - const result = {} - - parts.forEach((part) => { - const [key, value] = part.trim().split('=') - result[key] = value.replace(/"/g, '') - }) - - return result -} - -class APIError extends Error { - constructor(message, statusCode) { - super(message) - this.statusCode = statusCode - } -} - function getFetchOptions(token) { return { method: 'GET', @@ -362,24 +142,6 @@ app.use((err, req, res, next) => { }) }) -function managerFallback() { - return { - display_name: 'Mangler ansvarlig', - principal_name: 'Mangler ansvarlig', - } -} - -function sectionFallback(uniformName) { - return { - uniform_name: uniformName, - section_name: 'Mangler seksjon', - } -} - -function groupFallback() { - return { _embedded: { groups: [] }, count: '0' } -} - //const lightship = await createLightship(); // Replace above with below to get liveness and readiness probes when running locally const lightship = await createLightship({ detectKubernetes: false }) diff --git a/src/@types/error.ts b/src/@types/error.ts deleted file mode 100644 index 0093a56a..00000000 --- a/src/@types/error.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ErrorResponse { - error: Error -} - -export interface Error { - code: string - message: string -} diff --git a/src/@types/group.ts b/src/@types/group.ts deleted file mode 100644 index 44575cff..00000000 --- a/src/@types/group.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { User } from '../@types/user' - -export interface Group { - uniform_name: string - display_name: string - manager?: User -} diff --git a/src/@types/team.ts b/src/@types/team.ts deleted file mode 100644 index efcbf5a3..00000000 --- a/src/@types/team.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { User } from './user' - -export interface Team { - uniform_name: string - display_name: string - division_name: string - section_name: string - section_code: number - team_user_count: number - manager: User - groups?: string[] -} diff --git a/src/@types/user.ts b/src/@types/user.ts index 2cab9bde..e619f4b0 100644 --- a/src/@types/user.ts +++ b/src/@types/user.ts @@ -1,5 +1,3 @@ -import { Group } from '../@types/group' - export interface User { principal_name: string azure_ad_id: string @@ -11,7 +9,6 @@ export interface User { division_code?: number section_name?: string section_code?: number - manager?: User + section_manager?: User photo?: string - groups?: Group[] } diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index d1cf9859..d1ff44da 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -21,9 +21,7 @@ const Avatar = () => { if (!userProfile) return setUserProfileData(userProfile) - setEncodedURI( - `/teammedlemmer/${encodeURIComponent(userProfile.principal_name ? userProfile.principal_name.split('@')[0] : userProfile.email.split('@')[0])}` - ) + setEncodedURI(`/teammedlemmer/${userProfile.principal_name}`) setFallbackInitials(userProfile.first_name[0] + userProfile.last_name[0]) const base64Image = userProfile?.photo diff --git a/src/pages/TeamDetail/TeamDetail.tsx b/src/pages/TeamDetail/TeamDetail.tsx index 596a7e66..702ff49a 100644 --- a/src/pages/TeamDetail/TeamDetail.tsx +++ b/src/pages/TeamDetail/TeamDetail.tsx @@ -5,18 +5,19 @@ import { useCallback, useContext, useEffect, useState } from 'react' import PageLayout from '../../components/PageLayout/PageLayout' import { TeamDetailData, getTeamDetail } from '../../services/teamDetail' import { useParams } from 'react-router-dom' -import { ErrorResponse } from '../../@types/error' +import { ApiError } from '../../utils/services' + import { DaplaCtrlContext } from '../../provider/DaplaCtrlProvider' import Table, { TableData } from '../../components/Table/Table' import { formatDisplayName, getGroupType } from '../../utils/utils' -import { User } from '../../@types/user' +import { User } from '../../services/teamDetail' import { Text, Link, Dialog, LeadParagraph } from '@statisticsnorway/ssb-component-library' import PageSkeleton from '../../components/PageSkeleton/PageSkeleton' import { Skeleton } from '@mui/material' const TeamDetail = () => { const { setBreadcrumbTeamDetailDisplayName } = useContext(DaplaCtrlContext) - const [error, setError] = useState() + const [error, setError] = useState() const [loadingTeamData, setLoadingTeamData] = useState(true) const [teamDetailData, setTeamDetailData] = useState() const [teamDetailTableData, setTeamDetailTableData] = useState() @@ -24,7 +25,11 @@ const TeamDetail = () => { const { teamId } = useParams<{ teamId: string }>() const prepTeamData = useCallback((response: TeamDetailData): TableData['data'] => { - return response['teamUsers'].teamUsers.map((user) => { + if (!response.team.users) { + return [] + } + + return response.team.users.map((user) => { // Makes data in username column searchable and sortable in table by including these fields const usernameColumn = { user: user.display_name, @@ -35,7 +40,10 @@ const TeamDetail = () => { id: user?.principal_name, ...usernameColumn, navn: renderUsernameColumn(user), - gruppe: user.groups?.map((group) => getGroupType(group.uniform_name)).join(', '), + gruppe: user.groups + ?.filter((group) => group.uniform_name.startsWith(response.team.uniform_name)) + .map((group) => getGroupType(group.uniform_name)) + .join(', '), epost: user?.principal_name, } }) @@ -43,48 +51,35 @@ const TeamDetail = () => { useEffect(() => { if (!teamId) return - getTeamDetail(teamId as string) + getTeamDetail(teamId) .then((response) => { - if ((response as ErrorResponse).error) { - setError(response as ErrorResponse) - } else { - setTeamDetailData(response as TeamDetailData) - } + const formattedResponse = response as TeamDetailData + setTeamDetailData(formattedResponse) + + const displayName = formatDisplayName(formattedResponse.team.display_name) + setBreadcrumbTeamDetailDisplayName({ displayName }) }) .catch((error) => { - setError({ error: { message: error.message, code: '500' } }) + setError(error as ApiError) }) }, []) useEffect(() => { getTeamDetail(teamId as string) .then((response) => { - if ((response as ErrorResponse).error) { - setError(response as ErrorResponse) - } else { - setTeamDetailTableData(prepTeamData(response as TeamDetailData)) - } + setTeamDetailTableData(prepTeamData(response as TeamDetailData)) }) .finally(() => setLoadingTeamData(false)) .catch((error) => { - setError({ error: { message: error.message, code: '500' } }) + setError(error as ApiError) }) }, [prepTeamData]) - // required for breadcrumb - useEffect(() => { - if (!teamDetailData) return - - const displayName = teamDetailData['teamUsers'].teamInfo.display_name - teamDetailData['teamUsers'].teamInfo.display_name = displayName - setBreadcrumbTeamDetailDisplayName({ displayName }) - }, [teamDetailData, setBreadcrumbTeamDetailDisplayName]) - const renderUsernameColumn = (user: User) => { return ( <> - + {formatDisplayName(user.display_name)} @@ -95,8 +90,8 @@ const TeamDetail = () => { const renderErrorAlert = () => { return ( - - {error?.error.message} + + {`${error?.code} - ${error?.message}`} ) } @@ -124,12 +119,12 @@ const TeamDetail = () => { <> - {teamDetailData ? teamDetailData['teamUsers'].teamInfo.uniform_name : ''} + {teamDetailData ? teamDetailData.team.uniform_name : ''} - {teamDetailData ? formatDisplayName(teamDetailData['teamUsers'].teamInfo.manager.display_name) : ''} + {teamDetailData ? formatDisplayName(teamDetailData.team.manager?.display_name ?? '') : ''} - {teamDetailData ? teamDetailData['teamUsers'].teamInfo.section_name : ''} + {teamDetailData ? teamDetailData.team.section_name : ''} { ) diff --git a/src/pages/TeamOverview/TeamOverview.tsx b/src/pages/TeamOverview/TeamOverview.tsx index 7526840b..5760aea7 100644 --- a/src/pages/TeamOverview/TeamOverview.tsx +++ b/src/pages/TeamOverview/TeamOverview.tsx @@ -7,13 +7,14 @@ import PageLayout from '../../components/PageLayout/PageLayout' import Table, { TableData } from '../../components/Table/Table' import PageSkeleton from '../../components/PageSkeleton/PageSkeleton' -import { ErrorResponse } from '../../@types/error' -import { Team } from '../../@types/team' - -import { getTeamOverview, TeamOverviewData } from '../../services/teamOverview' +import { fetchTeamOverviewData, TeamOverviewData, Team } from '../../services/teamOverview' import { formatDisplayName } from '../../utils/utils' +import { ApiError } from '../../utils/services' const TeamOverview = () => { + const accessToken = localStorage.getItem('access_token') || '' + const jwt = JSON.parse(atob(accessToken.split('.')[1])) + const defaultActiveTab = { title: 'Mine team', path: 'myTeams', @@ -23,18 +24,17 @@ const TeamOverview = () => { const [teamOverviewData, setTeamOverviewData] = useState() const [teamOverviewTableData, setTeamOverviewTableData] = useState() const [teamOverviewTableTitle, setTeamOverviewTableTitle] = useState(defaultActiveTab.title) - const [error, setError] = useState() + const [error, setError] = useState() const [loading, setLoading] = useState(true) const prepTeamData = useCallback( (response: TeamOverviewData): TableData['data'] => { - const team = (activeTab as TabProps)?.path ?? activeTab - - return response[team].teams.map((team) => ({ + const teamTab = (activeTab as TabProps)?.path ?? activeTab + return response[teamTab].teams.map((team) => ({ id: team.uniform_name, seksjon: team.section_name, // Makes section name searchable and sortable in table by including the field navn: renderTeamNameColumn(team), - teammedlemmer: team.team_user_count, + teammedlemmer: team.users.length, ansvarlig: formatDisplayName(team.manager.display_name), })) }, @@ -42,18 +42,15 @@ const TeamOverview = () => { ) useEffect(() => { - getTeamOverview() + if (!jwt) return + fetchTeamOverviewData(jwt.email) .then((response) => { - if ((response as ErrorResponse).error) { - setError(response as ErrorResponse) - } else { - setTeamOverviewData(response as TeamOverviewData) - setTeamOverviewTableData(prepTeamData(response as TeamOverviewData)) - } + setTeamOverviewData(response as TeamOverviewData) + setTeamOverviewTableData(prepTeamData(response as TeamOverviewData)) }) .finally(() => setLoading(false)) .catch((error) => { - setError(error.toString()) + setError(error as ApiError) }) }, []) @@ -88,7 +85,7 @@ const TeamOverview = () => { const renderErrorAlert = () => { return ( - {error?.error.message} + {`${error?.code} - ${error?.message}`} ) } @@ -119,8 +116,8 @@ const TeamOverview = () => { onClick={handleTabClick} activeOnInit={defaultActiveTab.path} items={[ - { title: `Mine team (${teamOverviewData?.myTeams.count ?? 0})`, path: 'myTeams' }, - { title: `Alle team (${teamOverviewData?.allTeams.count ?? 0})`, path: 'allTeams' }, + { title: `Mine team (${teamOverviewData?.myTeams.teams.length ?? 0})`, path: 'myTeams' }, + { title: `Alle team (${teamOverviewData?.allTeams.teams.length ?? 0})`, path: 'allTeams' }, ]} /> diff --git a/src/pages/UserProfile/UserProfile.tsx b/src/pages/UserProfile/UserProfile.tsx index 5fa4ff81..5fa015c8 100644 --- a/src/pages/UserProfile/UserProfile.tsx +++ b/src/pages/UserProfile/UserProfile.tsx @@ -11,31 +11,32 @@ import { useCallback, useContext, useEffect, useState } from 'react' import { DaplaCtrlContext } from '../../provider/DaplaCtrlProvider' import { getGroupType, formatDisplayName } from '../../utils/utils' -import { getUserProfile, getUserTeamsWithGroups, UserProfileTeamResult } from '../../services/userProfile' - -import { User } from '../../@types/user' -import { Team } from '../../@types/team' +import { getUserProfileTeamData, TeamsData, Team } from '../../services/userProfile' import { useParams } from 'react-router-dom' -import { ErrorResponse } from '../../@types/error' import { Skeleton } from '@mui/material' +import { ApiError } from '../../utils/services' const UserProfile = () => { const { setBreadcrumbUserProfileDisplayName } = useContext(DaplaCtrlContext) - const [error, setError] = useState() + const [error, setError] = useState() const [loadingTeamData, setLoadingTeamData] = useState(true) - const [userProfileData, setUserProfileData] = useState() + const [userProfileData, setUserProfileData] = useState() const [teamUserProfileTableData, setUserProfileTableData] = useState() const { principalName } = useParams() const prepTeamData = useCallback( - (response: UserProfileTeamResult): TableData['data'] => { + (response: TeamsData): TableData['data'] => { return response.teams.map((team) => ({ id: team.uniform_name, seksjon: team.section_name, // Makes section name searchable and sortable in table by including the field navn: renderTeamNameColumn(team), - gruppe: team.groups?.map((group) => getGroupType(group)).join(', '), - epost: userProfileData?.principal_name, + gruppe: principalName + ? team.groups + ?.filter((group) => group.users.some((user) => user.principal_name === principalName)) // Filter groups based on principalName presence + .map((group) => getGroupType(group.uniform_name)) + .join(', ') + : 'INGEN FUNNET', ansvarlig: formatDisplayName(team.manager.display_name), })) }, @@ -43,43 +44,19 @@ const UserProfile = () => { ) useEffect(() => { - getUserProfile(principalName as string) + getUserProfileTeamData(principalName as string) .then((response) => { - if ((response as ErrorResponse).error) { - setError(response as ErrorResponse) - } else { - setUserProfileData(response as User) - } + const formattedResponse = response as TeamsData + setUserProfileTableData(prepTeamData(formattedResponse)) + setUserProfileData(formattedResponse) + + const displayName = formatDisplayName(formattedResponse.user.display_name) + setBreadcrumbUserProfileDisplayName({ displayName }) }) + .finally(() => setLoadingTeamData(false)) .catch((error) => { - setError({ error: { message: error.message, code: '500' } }) + setError(error as ApiError) }) - }, []) - - useEffect(() => { - if (userProfileData) { - getUserTeamsWithGroups(principalName as string) - .then((response) => { - if ((response as ErrorResponse).error) { - setError(response as ErrorResponse) - } else { - setUserProfileTableData(prepTeamData(response as UserProfileTeamResult)) - } - }) - .finally(() => setLoadingTeamData(false)) - .catch((error) => { - setError({ error: { message: error.message, code: '500' } }) - }) - } - }, [prepTeamData]) - - // required for breadcrumb - useEffect(() => { - if (userProfileData) { - const displayName = formatDisplayName(userProfileData.display_name) - userProfileData.display_name = displayName - setBreadcrumbUserProfileDisplayName({ displayName }) - } }, [setBreadcrumbUserProfileDisplayName]) const renderTeamNameColumn = (team: Team) => { @@ -97,8 +74,8 @@ const UserProfile = () => { const renderErrorAlert = () => { return ( - - {error?.error.message} + + {`${error?.code} - ${error?.message}`} ) } @@ -117,10 +94,6 @@ const UserProfile = () => { id: 'gruppe', label: 'Gruppe', }, - { - id: 'epost', - label: 'Epost ?', - }, { id: 'ansvarlig', label: 'Ansvarlig', @@ -129,8 +102,8 @@ const UserProfile = () => { return ( <> - {userProfileData?.section_name} - {userProfileData?.principal_name} + {userProfileData?.user.section_name} + {userProfileData?.user.principal_name}
{ ) diff --git a/src/services/teamDetail.ts b/src/services/teamDetail.ts index 7e890f0c..2fa31ee0 100644 --- a/src/services/teamDetail.ts +++ b/src/services/teamDetail.ts @@ -1,36 +1,109 @@ -import { Team } from '../@types/team' -import { User } from '../@types/user' -import { ErrorResponse } from '../@types/error' +import { ApiError, fetchAPIData } from '../utils/services' +import { flattenEmbedded } from '../utils/utils' + +const DAPLA_TEAM_API_URL = import.meta.env.VITE_DAPLA_TEAM_API_URL +const TEAMS_URL = `${DAPLA_TEAM_API_URL}/teams` export interface TeamDetailData { - [key: string]: TeamDetailTeamResult + team: Team } -export interface TeamDetailTeamResult { - teamInfo: Team - teamUsers: User[] - count: number +export interface Team { + uniform_name: string + division_name?: string + display_name: string + section_name: string + section_code?: string + manager?: TeamManager + users?: User[] + groups?: Group[] + // eslint-disable-next-line + _embedded?: any } -export const getTeamDetail = async (teamId: string): Promise => { - const accessToken = localStorage.getItem('access_token') +interface TeamManager { + display_name: string + principal_name: string +} + +export interface User { + display_name: string + principal_name: string + section_name: string + groups: Group[] +} + +interface Group { + uniform_name: string + display_name: string +} + +export const fetchTeamInfo = async (teamId: string, accessToken: string): Promise => { + const teamsUrl = new URL(`${TEAMS_URL}/${teamId}`) + const embeds = ['users', 'users.groups', 'managers'] + const selects = [ + 'uniform_name', + 'display_name', + 'section_name', + 'managers.principal_name', + 'managers.display_name', + 'managers.section_name', + 'users.principal_name', + 'users.display_name', + 'users.section_name', + 'users.groups.uniform_name', + ] + + teamsUrl.searchParams.set('embed', embeds.join(',')) + teamsUrl.searchParams.append('select', selects.join(',')) try { - const response = await fetch(`/api/teamDetail/${teamId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - }) - if (!response.ok) { - const errorData = await response.json() - return errorData as ErrorResponse + const teamDetailData = await fetchAPIData(teamsUrl.toString(), accessToken) + const flattendTeams = flattenEmbedded(teamDetailData) + if (!flattendTeams) return {} as Team + if (!flattendTeams.users) flattendTeams.users = [] + if (!flattendTeams.managers || flattendTeams.managers.length === 0) { + flattendTeams.manager = { + display_name: 'Ikke funnet', + principal_name: 'Ikke funnet', + section_name: 'Ikke funnet', + } + } else { + flattendTeams.manager = flattendTeams.managers[0] + } + delete flattendTeams.managers + + return flattendTeams + } catch (error) { + if (error instanceof ApiError) { + console.error('Failed to fetch teams:', error) + throw error + } else { + const apiError = new ApiError(500, 'An unexpected error occurred') + console.error('Failed to fetch teams:', apiError) + throw apiError } - const data = await response.json() - return data as TeamDetailData + } +} + +export const getTeamDetail = async (teamId: string): Promise => { + const accessToken = localStorage.getItem('access_token') as string + + try { + const [teamInfo] = await Promise.all([ + fetchTeamInfo(teamId, accessToken), + //TODO: Add shared buckets part + ]) + + return { team: teamInfo } as TeamDetailData } catch (error) { - console.error('Error during fetching teams:', error) - throw new Error('Error fetching teams') + if (error instanceof ApiError) { + console.error('Failed to fetch data for teamDetail page:', error) + throw error + } else { + const apiError = new ApiError(500, 'An unexpected error occurred') + console.error('FFailed to fetch data for teamDetail page:', apiError) + throw apiError + } } } diff --git a/src/services/teamOverview.ts b/src/services/teamOverview.ts index 94753a02..09cf2d42 100644 --- a/src/services/teamOverview.ts +++ b/src/services/teamOverview.ts @@ -1,34 +1,181 @@ -import { Team } from '../@types/team' -import { ErrorResponse } from '../@types/error' +import { ApiError, fetchAPIData } from '../utils/services' +import { flattenEmbedded } from '../utils/utils' +const DAPLA_TEAM_API_URL = import.meta.env.VITE_DAPLA_TEAM_API_URL +const USERS_URL = `${DAPLA_TEAM_API_URL}/users` +const TEAMS_URL = `${DAPLA_TEAM_API_URL}/teams` export interface TeamOverviewData { - [key: string]: TeamOverviewResult // myTeams, allTeams + [key: string]: TeamsData // myTeams, allTeams } -export interface TeamOverviewResult { +interface TeamsData { teams: Team[] - count: number } -export const getTeamOverview = async (): Promise => { - const accessToken = localStorage.getItem('access_token') +export interface Team { + uniform_name: string + division_name: string + display_name: string + section_name: string + section_code: string + manager: TeamManager + users: User[] + groups: Group[] + // eslint-disable-next-line + _embedded?: any +} + +interface TeamManager { + display_name: string + principal_name: string +} + +interface User { + display_name: string + principal_name: string +} + +interface Group { + uniform_name: string + display_name: string + users: User[] +} + +const fetchAllTeams = async (accessToken: string): Promise => { + const teamsUrl = new URL(`${TEAMS_URL}`) + const embeds = ['users', 'groups.users'] + + const selects = [ + 'display_name', + 'uniform_name', + 'section_name', + 'users.principal_name', + 'groups.users.display_name', + 'groups.users.principal_name', + ] + + teamsUrl.searchParams.set('embed', embeds.join(',')) + teamsUrl.searchParams.append('select', selects.join(',')) try { - const response = await fetch(`/api/teamOverview`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, + const teams = await fetchAPIData(teamsUrl.toString(), accessToken) + if (!teams) throw new ApiError(500, 'No json data returned') + if (!teams._embedded || !teams._embedded.teams) return {} as TeamsData + + const flattedTeams = flattenEmbedded({ ...teams }) + flattedTeams.teams.forEach((team: Team, teamIndex: number) => { + if (!team.groups) flattedTeams.teams[teamIndex].groups = [] + + team.groups.forEach((group: Group, groupIndex: number) => { + if (!group.users) { + flattedTeams.teams[teamIndex].groups[groupIndex].users = [] + } + }) + if (!team.users) flattedTeams.teams[teamIndex].users = [] }) - if (!response.ok) { - const errorData = await response.json() - return errorData as ErrorResponse + + const flattedTeamsWithManager = flattedTeams.teams.map((team: Team) => { + const managers = team.groups.find((group) => group.uniform_name === `${team.uniform_name}-managers`) + return { + ...team, + manager: + managers && managers.users && managers.users.length > 0 + ? { display_name: managers.users[0].display_name, principal_name: managers.users[0].principal_name } + : { display_name: 'Mangler manager', principal_name: 'ManglerManager@ssb.no' }, + } + }) + + return { teams: flattedTeamsWithManager } + } catch (error) { + if (error instanceof ApiError) { + console.error('Failed to fetch all teams:', error) + throw error + } else { + const apiError = new ApiError(500, 'An unexpected error occurred') + console.error('Failed to fetch all teams:', apiError) + throw apiError } - const data = await response.json() - return data as TeamOverviewData + } +} + +const fetchTeamsForPrincipalName = async (accessToken: string, principalName: string): Promise => { + const usersUrl = new URL(`${USERS_URL}/${principalName}`) + const embeds = ['teams', 'teams.users', 'teams.groups.users'] + + const selects = [ + 'display_name', + 'principal_name', + 'teams.display_name', + 'teams.uniform_name', + 'teams.section_name', + 'teams.users.principal_name', + 'teams.groups.users.display_name', + 'teams.groups.users.principal_name', + ] + + usersUrl.searchParams.set('embed', embeds.join(',')) + usersUrl.searchParams.append('select', selects.join(',')) + + try { + const teams = await fetchAPIData(usersUrl.toString(), accessToken) + + if (!teams) throw new ApiError(500, 'No json data returned') + if (!teams._embedded || !teams._embedded.teams) return {} as TeamsData + + const flattedTeams = flattenEmbedded({ ...teams }) + flattedTeams.teams.forEach((team: Team) => { + if (!team.groups) flattedTeams.teams.groups = [] + if (!team.users) flattedTeams.teams.users = [] + }) + + const flattedTeamsWithManager = flattedTeams.teams.map((team: Team) => { + const managers = team.groups.find((group) => group.uniform_name === `${team.uniform_name}-managers`) + return { + ...team, + manager: + managers && managers.users && managers.users.length > 0 + ? { display_name: managers.users[0].display_name, principal_name: managers.users[0].principal_name } + : { display_name: 'Mangler ansvarlig', principal_name: 'ManglerAnsvarlig@ssb.no' }, + } + }) + + return { teams: flattedTeamsWithManager } + } catch (error) { + if (error instanceof ApiError) { + console.error('Failed to fetch teams for PrincipalName:', error) + throw error + } else { + const apiError = new ApiError(500, 'An unexpected error occurred') + console.error('Failed to fetch teams for PrincipalName:', apiError) + throw apiError + } + } +} + +export const fetchTeamOverviewData = 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 [myTeams, allTeams] = await Promise.all([ + fetchTeamsForPrincipalName(accessToken, principalName), + fetchAllTeams(accessToken), + ]) + + return { myTeams: myTeams, allTeams: allTeams } as TeamOverviewData } catch (error) { - console.error('Error during fetching teams:', error) - throw new Error('Error fetching teams') + if (error instanceof ApiError) { + console.error('Failed to fetch team overview 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 overview data:', apiError) + throw apiError + } } } diff --git a/src/services/userProfile.ts b/src/services/userProfile.ts index bbd3bf26..a7b110a6 100644 --- a/src/services/userProfile.ts +++ b/src/services/userProfile.ts @@ -1,64 +1,155 @@ -import { User } from '../@types/user' -import { Team } from '../@types/team' -import { ErrorResponse } from '../@types/error' +import { ApiError, fetchAPIData } from '../utils/services' +import { flattenEmbedded } from '../utils/utils' + +const DAPLA_TEAM_API_URL = import.meta.env.VITE_DAPLA_TEAM_API_URL +const USERS_URL = `${DAPLA_TEAM_API_URL}/users` export interface UserProfileTeamData { - [key: string]: UserProfileTeamResult + [key: string]: TeamsData } -export interface UserProfileTeamResult { +export interface TeamsData { + user: User teams: Team[] - count: number } -export const getUserProfile = async (principalName: string, token?: string): Promise => { - const accessToken = localStorage.getItem('access_token') +export interface Team { + uniform_name: string + division_name: string + display_name: string + section_name: string + section_code: string + manager: TeamManager + users: User[] + groups: Group[] + // eslint-disable-next-line + _embedded?: any +} + +interface TeamManager { + display_name: string + principal_name: string +} + +interface User { + display_name: string + principal_name: string + section_name: string + azure_ad_id?: string + first_name?: string + last_name?: string + email?: string +} + +interface Group { + uniform_name: string + display_name: string + users: User[] +} + +export const getUserProfile = async (principalName: string, token?: string): Promise => { + const accessToken = (localStorage.getItem('access_token') as string) || (token as string) principalName = principalName.replace(/@ssb\.no$/, '') + '@ssb.no' - return fetch(`/api/userProfile/${principalName}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken || token}`, - }, - }) - .then((response) => { - if (!response.ok) { - console.error('Request failed with status:', response.status) - throw new Error('Request failed') - } - return response.json() - }) - .then((data) => data as User) - .catch((error) => { - console.error('Error during fetching userProfile:', error) + const usersUrl = new URL(`${USERS_URL}/${principalName}`) + + const embeds = ['section_manager'] + const selects = [ + 'principal_name', + 'display_name', + 'first_name', + 'last_name', + 'section_name', + 'division_name', + 'phone', + 'section_manager.display_name', + 'section_manager.principal_name', + ] + + usersUrl.searchParams.set('embed', embeds.join(',')) + usersUrl.searchParams.append('select', selects.join(',')) + + try { + const [userData, userPhoto] = await Promise.all([ + fetchAPIData(usersUrl.toString(), accessToken), + fetchPhoto(accessToken, principalName), + ]) + + userData.photo = userPhoto + + return flattenEmbedded({ ...userData }) + } catch (error) { + if (error instanceof ApiError) { + console.error('Failed to fetch userProfile data:', error) throw error - }) + } else { + const apiError = new ApiError(500, 'An unexpected error occurred') + console.error('Failed to fetch userProfile data:', apiError) + throw apiError + } + } } -export const getUserTeamsWithGroups = async (principalName: string): Promise => { - const accessToken = localStorage.getItem('access_token') - principalName = principalName.replace(/@ssb\.no$/, '') + '@ssb.no' +export const getUserProfileTeamData = async (principalName: string): Promise => { + const accessToken = localStorage.getItem('access_token') as string + principalName.replace(/@ssb\.no$/, '') + '@ssb.no' - return fetch(`/api/userProfile/${principalName}/team`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - }) - .then((response) => { - if (!response.ok) { - console.error('Request failed with status:', response.status) - throw new Error('Request failed') + const usersUrl = new URL(`${USERS_URL}/${principalName}`) + const embeds = ['teams', 'teams.groups', 'teams.groups.users'] + + const selects = [ + 'display_name', + 'principal_name', + 'teams.section_name', + 'teams.display_name', + 'teams.uniform_name', + 'team.groups.uniform_name', + 'teams.groups.users.principal_name', + 'teams.groups.users.display_name', + ] + + usersUrl.searchParams.set('embed', embeds.join(',')) + usersUrl.searchParams.append('select', selects.join(',')) + + try { + const userProfileData = await fetchAPIData(usersUrl.toString(), accessToken) + + if (!userProfileData) throw new ApiError(500, 'No json data returned') + if (!userProfileData._embedded || !userProfileData._embedded.teams) return {} as TeamsData + + const flattedTeams = flattenEmbedded({ ...userProfileData }) + flattedTeams.teams.forEach((team: Team, teamIndex: number) => { + if (!team.groups) flattedTeams.teams.groups = [] + + team.groups.forEach((group: Group, groupIndex: number) => { + if (!group.users) flattedTeams.teams[teamIndex].groups[groupIndex].users = [] + }) + }) + + const flattedTeamsWithManager = flattedTeams.teams.map((team: Team) => { + const managers = team.groups.find((group) => group.uniform_name === `${team.uniform_name}-managers`) + return { + ...team, + manager: + managers && managers.users && managers.users.length > 0 + ? { display_name: managers.users[0].display_name, principal_name: managers.users[0].principal_name } + : { display_name: 'Mangler ansvarlig', principal_name: 'ManglerAnsvarlig@ssb.no' }, } - return response.json() }) - .then((data) => data as UserProfileTeamResult) - .catch((error) => { - console.error('Error during fetching userProfile:', error) + + const getUser = await getUserProfile(principalName) + + return { teams: flattedTeamsWithManager, user: getUser as User } + } catch (error) { + if (error instanceof ApiError) { + console.error('Failed to fetch teams for PrincipalName:', error) throw error - }) + } else { + const apiError = new ApiError(500, 'An unexpected error occurred') + console.error('Failed to fetch teams for PrincipalName:', apiError) + throw apiError + } + } } export const getUserProfileFallback = (accessToken: string): User => { @@ -67,8 +158,26 @@ export const getUserProfileFallback = (accessToken: string): User => { principal_name: jwt.upn, azure_ad_id: jwt.oid, // not the real azureAdId, this is actually keycloaks oid display_name: jwt.name, + section_name: 'UNSET', first_name: jwt.given_name, last_name: jwt.family_name, email: jwt.email, } } + +const fetchPhoto = async (accessToken: string, principalName: string) => { + const response = await fetch(`/api/photo/${principalName}`, { + method: 'GET', + headers: { + Accept: '*/*', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + throw new ApiError(500, 'could not fetch photo') + } + + const data = await response.json() + return data.photo +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 761b62bc..e9a0f171 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -20,3 +20,21 @@ export const getGroupType = (groupName: string) => { export const formatDisplayName = (displayName: string) => { return displayName.split(', ').reverse().join(' ') } + +// eslint-disable-next-line +export const flattenEmbedded = (json: any): any => { + if (json._embedded) { + for (const prop in json._embedded) { + json[prop] = json._embedded[prop] + } + delete json._embedded + } + + for (const prop in json) { + if (typeof json[prop] === 'object') { + json[prop] = flattenEmbedded(json[prop]) + } + } + + return json +}