diff --git a/src/App.tsx b/src/App.tsx index 04da9cc5..8eb527ec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,7 @@ import { Routes, Route } from 'react-router-dom' export default function App() { return ( - } /> + {import.meta.env.NODE_ENV !== 'production' && } />} }> {/* Possibly setup passable props to ProtectedRoute so we can add authorization too, example: diff --git a/src/ProtectedRoute.tsx b/src/ProtectedRoute.tsx new file mode 100644 index 00000000..a1fb54d7 --- /dev/null +++ b/src/ProtectedRoute.tsx @@ -0,0 +1,30 @@ +import { useEffect, useState } from 'react' +import { useNavigate, Outlet } from 'react-router-dom' +import { validateKeycloakToken } from './services/validateKeycloakToken' + +const ProtectedRoute = () => { + const [isAuthenticated, setIsAuthenticated] = useState(false) + const navigate = useNavigate() + const from = location.pathname + + useEffect(() => { + if (localStorage.getItem('userProfile') === null) { + localStorage.removeItem('access_token') + navigate('/login', { state: { from: from } }) + return + } + + validateKeycloakToken().then((isValid) => { + setIsAuthenticated(isValid) + if (!isValid) { + localStorage.removeItem('access_token') + localStorage.removeItem('userProfile') + navigate('/login', { state: { from: from } }) + } + }) + }, [from, navigate]) + + return isAuthenticated ? : null +} + +export default ProtectedRoute diff --git a/src/components/Header/header.module.scss b/src/components/Header/header.module.scss index 0fe8c999..7905437c 100644 --- a/src/components/Header/header.module.scss +++ b/src/components/Header/header.module.scss @@ -8,10 +8,16 @@ color: variables.$ssb-dark-5; align-items: center; padding: 1rem; - + padding-left: 4rem; + margin: 0; + font-family: 'Roboto Condensed', sans-serif !important; + font-stretch: condensed; + font-weight: bold; + font-size: 2rem; + .navigation { @extend .header; - gap: 2rem; + gap: 5rem; border-bottom: 0; // unset border-bottom from the inherited .header class padding: 0; // unset padding } @@ -22,4 +28,13 @@ border-bottom: 0; // unset border-bottom from the inherited .header class padding: 0; // unset padding } + + @media #{variables.$mobile} { + margin: 0; + padding-right: 0; + padding-left: .75rem; + .navigation { + gap: 1rem; + } + } } \ No newline at end of file diff --git a/src/components/PageLayout/PageLayout.tsx b/src/components/PageLayout/PageLayout.tsx index a01c6278..7ea1f5a0 100644 --- a/src/components/PageLayout/PageLayout.tsx +++ b/src/components/PageLayout/PageLayout.tsx @@ -2,26 +2,24 @@ import Breadcrumb from '../Breadcrumb' import Header from '../Header/Header' import styles from './pagelayout.module.scss' -import { Title, LeadParagraph } from '@statisticsnorway/ssb-component-library' +import { Title } from '@statisticsnorway/ssb-component-library' interface PageLayoutProps { - title: string - description?: JSX.Element + title?: string | JSX.Element button?: JSX.Element content?: JSX.Element } -export default function PageLayout({ title, description, button, content }: PageLayoutProps) { +export default function PageLayout({ title, button, content }: PageLayoutProps) { return ( <>
- {title} + {title && {title}} {button}
- {description} {content}
diff --git a/src/components/PageLayout/pagelayout.module.scss b/src/components/PageLayout/pagelayout.module.scss index 0dc3d565..6c0ece4d 100644 --- a/src/components/PageLayout/pagelayout.module.scss +++ b/src/components/PageLayout/pagelayout.module.scss @@ -1,7 +1,7 @@ @use '@statisticsnorway/ssb-component-library/src/style/variables' as variables; .container { - margin: 1.5rem 8rem; + margin: 1.5rem 12.25rem; @media #{variables.$mobile} { margin: 1.5rem; diff --git a/src/components/PageSkeleton/PageSkeleton.tsx b/src/components/PageSkeleton/PageSkeleton.tsx new file mode 100644 index 00000000..cfbfecab --- /dev/null +++ b/src/components/PageSkeleton/PageSkeleton.tsx @@ -0,0 +1,22 @@ +import { Skeleton } from '@mui/material' +import styles from './pageSkeleton.module.scss' + +interface PageSkeletonProps { + hasDescription?: boolean + hasTab?: boolean +} + +const PageSkeleton = ({ hasDescription, hasTab = true }: PageSkeletonProps) => { + return ( + <> + {hasDescription && ( + + )} + {hasTab && } + + + + ) +} + +export default PageSkeleton diff --git a/src/components/PageSkeleton/pageSkeleton.module.scss b/src/components/PageSkeleton/pageSkeleton.module.scss new file mode 100644 index 00000000..f870484c --- /dev/null +++ b/src/components/PageSkeleton/pageSkeleton.module.scss @@ -0,0 +1,9 @@ +@use '@statisticsnorway/ssb-component-library/src/style/variables' as variables; + +.description { + margin-top: -2rem; + + @media #{variables.$mobile} { + margin-top: -1.5rem; + } +} \ No newline at end of file diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 718fafff..9e10766b 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useNavigate, Outlet } from 'react-router-dom' -import { validateKeycloakToken } from '../api/validateKeycloakToken' +import { validateKeycloakToken } from '../services/validateKeycloakToken' const ProtectedRoute = () => { const [isAuthenticated, setIsAuthenticated] = useState(false) diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index c7b54b64..6e87f092 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -5,8 +5,8 @@ import { useLocation, useNavigate } from 'react-router-dom' import { Title, Input, Link } from '@statisticsnorway/ssb-component-library' -import { validateKeycloakToken } from '../../api/validateKeycloakToken' -import { getUserProfile, getUserProfileFallback } from '../../api/userProfile' +import { validateKeycloakToken } from '../../services/validateKeycloakToken' +import { getUserProfile, getUserProfileFallback } from '../../services/userProfile' import { jwtRegex } from '../../utils/regex' export default function Login() { diff --git a/src/pages/TeamDetail/TeamDetail.tsx b/src/pages/TeamDetail/TeamDetail.tsx index 53653b9c..3ba8ae4f 100644 --- a/src/pages/TeamDetail/TeamDetail.tsx +++ b/src/pages/TeamDetail/TeamDetail.tsx @@ -3,14 +3,15 @@ import styles from './teamDetail.module.scss' import { useCallback, useContext, useEffect, useState } from 'react' import PageLayout from '../../components/PageLayout/PageLayout' -import { TeamDetailData, getTeamDetail } from '../../api/teamDetail' +import { TeamDetailData, getTeamDetail } from '../../services/teamDetail' import { useParams } from 'react-router-dom' import { ErrorResponse } from '../../@types/error' import { DaplaCtrlContext } from '../../provider/DaplaCtrlProvider' import Table, { TableData } from '../../components/Table/Table' -import { getGroupType } from '../../utils/utils' +import { formatDisplayName, getGroupType } from '../../utils/utils' import { User } from '../../@types/user' -import { Text, Title, Link, Dialog } from '@statisticsnorway/ssb-component-library' +import { Text, Title, Link, Dialog, LeadParagraph } from '@statisticsnorway/ssb-component-library' +import PageSkeleton from '../../components/PageSkeleton/PageSkeleton' import { Skeleton } from '@mui/material' export default function TeamDetail() { @@ -38,7 +39,6 @@ export default function TeamDetail() { if ((response as ErrorResponse).error) { setError(response as ErrorResponse) } else { - console.log(response) setTeamDetailData(response as TeamDetailData) } }) @@ -76,7 +76,7 @@ export default function TeamDetail() { <> - {user.display_name.split(', ').reverse().join(' ')} + {formatDisplayName(user.display_name)} {user && {user.section_name ? user.section_name : 'Mangler seksjon'}} @@ -92,18 +92,9 @@ export default function TeamDetail() { ) } - function renderSkeletonOnLoad() { - return ( - <> - - - - ) - } - function renderContent() { if (error) return renderErrorAlert() - if (loadingTeamData) return renderSkeletonOnLoad() + if (loadingTeamData) return // TODO: Remove hasTab prop after tabs are implemented if (teamDetailTableData) { const teamOverviewTableHeaderColumns = [ @@ -122,6 +113,15 @@ export default function TeamDetail() { ] return ( <> + + + {teamDetailData ? teamDetailData['teamUsers'].teamInfo.uniform_name : ''} + + + {teamDetailData ? formatDisplayName(teamDetailData['teamUsers'].teamInfo.manager.display_name) : ''} + + {teamDetailData ? teamDetailData['teamUsers'].teamInfo.section_name : ''} + Teammedlemmer @@ -133,19 +133,14 @@ export default function TeamDetail() { return ( - {teamDetailData ? teamDetailData['teamUsers'].teamInfo.uniform_name : ''} - - {teamDetailData - ? teamDetailData['teamUsers'].teamInfo.manager.display_name.split(', ').reverse().join(' ') - : ''} - - {teamDetailData ? teamDetailData['teamUsers'].teamInfo.section_name : ''} - + title={ + !loadingTeamData && teamDetailData ? ( + teamDetailData['teamUsers'].teamInfo.display_name + ) : ( + + ) } + content={renderContent()} /> ) } diff --git a/src/pages/TeamDetail/teamDetail.module.scss b/src/pages/TeamDetail/teamDetail.module.scss index 80c4bcb7..6e029044 100644 --- a/src/pages/TeamDetail/teamDetail.module.scss +++ b/src/pages/TeamDetail/teamDetail.module.scss @@ -3,6 +3,10 @@ .userProfileDescription { display: flex; flex-direction: column; - margin-top: -3rem; + margin-top: -2rem; gap: 0.5rem; +} + +.uniformName { + margin-bottom: .5rem; } \ No newline at end of file diff --git a/src/pages/TeamOverview/TeamOverview.tsx b/src/pages/TeamOverview/TeamOverview.tsx index c317c7c1..dfadf85f 100644 --- a/src/pages/TeamOverview/TeamOverview.tsx +++ b/src/pages/TeamOverview/TeamOverview.tsx @@ -2,16 +2,17 @@ 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 Skeleton from '@mui/material/Skeleton' 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 { ErrorResponse } from '../../@types/error' import { Team } from '../../@types/team' -import { getTeamOverview, TeamOverviewData } from '../../api/teamOverview' +import { getTeamOverview, TeamOverviewData } from '../../services/teamOverview' +import { formatDisplayName } from '../../utils/utils' export default function TeamOverview() { const defaultActiveTab = { @@ -34,7 +35,7 @@ export default function TeamOverview() { id: team.uniform_name, navn: renderTeamNameColumn(team), teammedlemmer: team.team_user_count, - ansvarlig: team.manager.display_name.split(', ').reverse().join(' '), + ansvarlig: formatDisplayName(team.manager.display_name), })) }, [activeTab] @@ -92,19 +93,9 @@ export default function TeamOverview() { ) } - function renderSkeletonOnLoad() { - return ( - <> - - - - - ) - } - function renderContent() { if (error) return renderErrorAlert() - if (loading) return renderSkeletonOnLoad() + if (loading) return if (teamOverviewTableData) { const teamOverviewTableHeaderColumns = [ diff --git a/src/pages/UserProfile/UserProfile.tsx b/src/pages/UserProfile/UserProfile.tsx index 1101114f..5d85cf44 100644 --- a/src/pages/UserProfile/UserProfile.tsx +++ b/src/pages/UserProfile/UserProfile.tsx @@ -1,28 +1,28 @@ import pageLayoutStyles from '../../components/PageLayout/pagelayout.module.scss' import styles from './userprofile.module.scss' -import { Dialog, Title, Text, Link } from '@statisticsnorway/ssb-component-library' -import { Skeleton } from '@mui/material' +import { Dialog, Title, Text, Link, LeadParagraph } from '@statisticsnorway/ssb-component-library' import Table, { TableData } from '../../components/Table/Table' import PageLayout from '../../components/PageLayout/PageLayout' +import PageSkeleton from '../../components/PageSkeleton/PageSkeleton' import { useCallback, useContext, useEffect, useState } from 'react' import { DaplaCtrlContext } from '../../provider/DaplaCtrlProvider' -import { getGroupType } from '../../utils/utils' +import { getGroupType, formatDisplayName } from '../../utils/utils' -import { getUserProfile, getUserTeamsWithGroups, UserProfileTeamResult } from '../../api/userProfile' +import { getUserProfile, getUserTeamsWithGroups, UserProfileTeamResult } from '../../services/userProfile' import { User } from '../../@types/user' import { Team } from '../../@types/team' import { useParams } from 'react-router-dom' import { ErrorResponse } from '../../@types/error' +import { Skeleton } from '@mui/material' export default function UserProfile() { const { setBreadcrumbUserProfileDisplayName } = useContext(DaplaCtrlContext) const [error, setError] = useState() - const [loadingUserProfileData, setLoadingUserProfileData] = useState(true) const [loadingTeamData, setLoadingTeamData] = useState(true) const [userProfileData, setUserProfileData] = useState() const [teamUserProfileTableData, setUserProfileTableData] = useState() @@ -35,7 +35,7 @@ export default function UserProfile() { navn: renderTeamNameColumn(team), gruppe: team.groups?.map((group) => getGroupType(group)).join(', '), epost: userProfileData?.principal_name, - ansvarlig: team.manager.display_name.split(', ').reverse().join(' '), + ansvarlig: formatDisplayName(team.manager.display_name), })) }, [userProfileData] @@ -50,7 +50,6 @@ export default function UserProfile() { setUserProfileData(response as User) } }) - .finally(() => setLoadingUserProfileData(false)) .catch((error) => { setError({ error: { message: error.message, code: '500' } }) }) @@ -76,7 +75,7 @@ export default function UserProfile() { // required for breadcrumb useEffect(() => { if (userProfileData) { - const displayName = userProfileData.display_name.split(', ').reverse().join(' ') + const displayName = formatDisplayName(userProfileData.display_name) userProfileData.display_name = displayName setBreadcrumbUserProfileDisplayName({ displayName }) } @@ -103,19 +102,9 @@ export default function UserProfile() { ) } - function renderSkeletonOnLoad() { - return ( - <> - - - - ) - } - function renderContent() { if (error) return renderErrorAlert() - // TODO: cheesy method to exclude showing skeleton for profile information (username etc..) - if (loadingTeamData && !loadingUserProfileData) return renderSkeletonOnLoad() + if (loadingTeamData) return // TODO: Remove hasTab prop after tabs are implemented if (teamUserProfileTableData) { const teamOverviewTableHeaderColumns = [ @@ -138,6 +127,10 @@ export default function UserProfile() { ] return ( <> + + {userProfileData?.section_name} + {userProfileData?.principal_name} + Team @@ -149,14 +142,14 @@ export default function UserProfile() { return ( - {userProfileData?.section_name} - {userProfileData?.principal_name} - + title={ + !loadingTeamData && userProfileData ? ( + (userProfileData?.display_name as string) + ) : ( + + ) } + content={renderContent()} /> ) } diff --git a/src/pages/UserProfile/userprofile.module.scss b/src/pages/UserProfile/userprofile.module.scss index 80c4bcb7..1fda3067 100644 --- a/src/pages/UserProfile/userprofile.module.scss +++ b/src/pages/UserProfile/userprofile.module.scss @@ -3,6 +3,6 @@ .userProfileDescription { display: flex; flex-direction: column; - margin-top: -3rem; gap: 0.5rem; + margin-top: -2rem; } \ No newline at end of file diff --git a/src/api/teamDetail.ts b/src/services/teamDetail.ts similarity index 100% rename from src/api/teamDetail.ts rename to src/services/teamDetail.ts diff --git a/src/api/teamOverview.ts b/src/services/teamOverview.ts similarity index 100% rename from src/api/teamOverview.ts rename to src/services/teamOverview.ts diff --git a/src/api/userProfile.ts b/src/services/userProfile.ts similarity index 100% rename from src/api/userProfile.ts rename to src/services/userProfile.ts diff --git a/src/api/validateKeycloakToken.ts b/src/services/validateKeycloakToken.ts similarity index 100% rename from src/api/validateKeycloakToken.ts rename to src/services/validateKeycloakToken.ts diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d8451631..761b62bc 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,4 +1,4 @@ -export function getGroupType(groupName: string) { +export const getGroupType = (groupName: string) => { const match = groupName.match(/(managers|developers|data-admins|support|consumers)$/) const role = match ? match[0] : null switch (role) { @@ -16,3 +16,7 @@ export function getGroupType(groupName: string) { return groupName } } + +export const formatDisplayName = (displayName: string) => { + return displayName.split(', ').reverse().join(' ') +} diff --git a/tsconfig.json b/tsconfig.json index f12903d1..b2e3e7ea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,12 +16,12 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, + "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [ { - "path": "./tsconfig.node.json", - }, - ], + "path": "./tsconfig.node.json" + } + ] }