diff --git a/src/App.tsx b/src/App.tsx index ef21faad..00a74fb3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ 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 SharedBucketDetail from './pages/SharedBucketDetail/SharedBucketDetail.tsx' import { Routes, Route } from 'react-router-dom' @@ -16,6 +17,7 @@ const App = () => { } /> } /> } /> + } /> } /> diff --git a/src/components/PageLayout/pagelayout.module.scss b/src/components/PageLayout/pagelayout.module.scss index cc858fbc..47d208fb 100644 --- a/src/components/PageLayout/pagelayout.module.scss +++ b/src/components/PageLayout/pagelayout.module.scss @@ -18,4 +18,15 @@ @media #{variables.$mobile} { flex-direction: column; } +} + +.description { + display: flex; + flex-direction: column; + margin-top: -2rem; + gap: 0.5rem; +} + +.descriptionSpacing { + margin-bottom: .5rem; } \ No newline at end of file diff --git a/src/pages/SharedBucketDetail/SharedBucketDetail.tsx b/src/pages/SharedBucketDetail/SharedBucketDetail.tsx new file mode 100644 index 00000000..8b230e46 --- /dev/null +++ b/src/pages/SharedBucketDetail/SharedBucketDetail.tsx @@ -0,0 +1,102 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import styles from '../../components/PageLayout/pagelayout.module.scss' + +import PageLayout from '../../components/PageLayout/PageLayout' +import PageSkeleton from '../../components/PageSkeleton/PageSkeleton' +import FormattedTableColumn from '../../components/FormattedTableColumn' +import Table, { TableData } from '../../components/Table/Table' +import { ApiError } from '../../utils/services' +import { Team, SharedBucket, SharedBucketDetail, getSharedBucketDetailData } from '../../services/sharedBucketDetail' + +import { DaplaCtrlContext } from '../../provider/DaplaCtrlProvider' + +import { useState, useEffect, useContext } from 'react' +import { useParams } from 'react-router-dom' +import { Dialog, LeadParagraph, Text } from '@statisticsnorway/ssb-component-library' + +const SharedBucketDetail = () => { + const { setBreadcrumbTeamDetailDisplayName, setBreadcrumbBucketDetailDisplayName } = useContext(DaplaCtrlContext) + const [error, setError] = useState() + const [loadingSharedBucketsData, setLoadingSharedBucketsData] = useState(true) + const [sharedBucketData, setSharedBucketData] = useState() + const [sharedBucketTableData, setSharedBucketTableData] = useState() + + const { teamId } = useParams<{ teamId: string }>() + const { shortName } = useParams<{ shortName: string }>() + + const prepSharedBucketTableData = (response: SharedBucketDetail): TableData['data'] => { + return (response['sharedBucket'] as SharedBucket).teams.map(({ display_name, uniform_name, section_name }) => { + return { + id: display_name ?? '', + team: , + } + }) + } + + useEffect(() => { + if (!teamId && !shortName) return + getSharedBucketDetailData(teamId as string, shortName as string) + .then((response) => { + setSharedBucketData(response) + setSharedBucketTableData(prepSharedBucketTableData(response)) + + const teamDisplayName = (response.team as Team).display_name as string + setBreadcrumbTeamDetailDisplayName({ displayName: teamDisplayName }) + + const bucketDisplayName = (response.sharedBucket as SharedBucket).bucket_name + setBreadcrumbBucketDetailDisplayName({ displayName: bucketDisplayName }) + }) + .finally(() => setLoadingSharedBucketsData(false)) + .catch((error) => { + setError(error as ApiError) + }) + }, []) + + const renderErrorAlert = () => { + return ( + + {`${error?.code} - ${error?.message}`} + + ) + } + + const renderContent = () => { + if (error) return renderErrorAlert() + if (loadingSharedBucketsData) return + + if (sharedBucketData) { + const sharedBucketsTableHeaderColumns = [ + { + id: 'team', + label: 'Team', + }, + ] + + return ( + <> + + + {(sharedBucketData.sharedBucket as SharedBucket).bucket_name} + + {(sharedBucketData.team as Team).display_name} + {(sharedBucketData.team as Team).section_name} + + + + ) + } + } + + return ( + + ) +} + +export default SharedBucketDetail diff --git a/src/pages/SharedBucketDetail/sharedBucketDetail.module.scss b/src/pages/SharedBucketDetail/sharedBucketDetail.module.scss new file mode 100644 index 00000000..1ba63990 --- /dev/null +++ b/src/pages/SharedBucketDetail/sharedBucketDetail.module.scss @@ -0,0 +1 @@ +@use '@statisticsnorway/ssb-component-library/src/style/variables' as variables; diff --git a/src/pages/TeamDetail/TeamDetail.tsx b/src/pages/TeamDetail/TeamDetail.tsx index 37f83280..e76c1e74 100644 --- a/src/pages/TeamDetail/TeamDetail.tsx +++ b/src/pages/TeamDetail/TeamDetail.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import styles from './teamDetail.module.scss' +import styles from '../../components/PageLayout/pagelayout.module.scss' import { TabProps } from '../../@types/pageTypes' @@ -158,8 +158,8 @@ const TeamDetail = () => { return ( <> - - + + {(teamDetailData.team as Team).uniform_name ?? ''} {formatDisplayName((teamDetailData.team as Team).manager?.display_name ?? '')} diff --git a/src/pages/TeamDetail/teamDetail.module.scss b/src/pages/TeamDetail/teamDetail.module.scss index 6e029044..24cbd24e 100644 --- a/src/pages/TeamDetail/teamDetail.module.scss +++ b/src/pages/TeamDetail/teamDetail.module.scss @@ -1,12 +1 @@ -@use '@statisticsnorway/ssb-component-library/src/style/variables' as variables; - -.userProfileDescription { - display: flex; - flex-direction: column; - margin-top: -2rem; - gap: 0.5rem; -} - -.uniformName { - margin-bottom: .5rem; -} \ No newline at end of file +@use '@statisticsnorway/ssb-component-library/src/style/variables' as variables; \ No newline at end of file diff --git a/src/pages/UserProfile/UserProfile.tsx b/src/pages/UserProfile/UserProfile.tsx index 137420d0..5598d5fc 100644 --- a/src/pages/UserProfile/UserProfile.tsx +++ b/src/pages/UserProfile/UserProfile.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import styles from './userprofile.module.scss' +import styles from '../../components/PageLayout/pagelayout.module.scss' import { Dialog, Text, LeadParagraph } from '@statisticsnorway/ssb-component-library' @@ -89,7 +89,7 @@ const UserProfile = () => { ] return ( <> - + {userProfileData?.user.section_name} {userProfileData?.user.principal_name} diff --git a/src/pages/UserProfile/userprofile.module.scss b/src/pages/UserProfile/userprofile.module.scss index 1fda3067..24cbd24e 100644 --- a/src/pages/UserProfile/userprofile.module.scss +++ b/src/pages/UserProfile/userprofile.module.scss @@ -1,8 +1 @@ -@use '@statisticsnorway/ssb-component-library/src/style/variables' as variables; - -.userProfileDescription { - display: flex; - flex-direction: column; - gap: 0.5rem; - margin-top: -2rem; -} \ No newline at end of file +@use '@statisticsnorway/ssb-component-library/src/style/variables' as variables; \ No newline at end of file diff --git a/src/provider/DaplaCtrlProvider.tsx b/src/provider/DaplaCtrlProvider.tsx index 98206d10..d4144690 100644 --- a/src/provider/DaplaCtrlProvider.tsx +++ b/src/provider/DaplaCtrlProvider.tsx @@ -8,6 +8,10 @@ interface BreadcrumbTeamDetailDisplayName { displayName: string } +interface BreadcrumbBucketDetailDisplayName { + displayName: string +} + interface DaplaCtrlContextType { breadcrumbUserProfileDisplayName: BreadcrumbUserProfileDisplayName | null setBreadcrumbUserProfileDisplayName: ( @@ -16,6 +20,11 @@ interface DaplaCtrlContextType { breadcrumbTeamDetailDisplayName: BreadcrumbTeamDetailDisplayName | null setBreadcrumbTeamDetailDisplayName: (breadcrumbTeamDetailDisplayName: BreadcrumbTeamDetailDisplayName | null) => void + + breadcrumbBucketDetailDisplayName: BreadcrumbBucketDetailDisplayName | null + setBreadcrumbBucketDetailDisplayName: ( + breadcrumbBucketDetailDisplayName: BreadcrumbBucketDetailDisplayName | null + ) => void } const DaplaCtrlContext = createContext({ @@ -23,6 +32,8 @@ const DaplaCtrlContext = createContext({ setBreadcrumbUserProfileDisplayName: () => {}, breadcrumbTeamDetailDisplayName: null, setBreadcrumbTeamDetailDisplayName: () => {}, + breadcrumbBucketDetailDisplayName: null, + setBreadcrumbBucketDetailDisplayName: () => {}, }) const DaplaCtrlProvider: FC<{ children: ReactNode }> = ({ children }) => { @@ -30,6 +41,8 @@ const DaplaCtrlProvider: FC<{ children: ReactNode }> = ({ children }) => { useState(null) const [breadcrumbTeamDetailDisplayName, setBreadcrumbTeamDetailDisplayName] = useState(null) + const [breadcrumbBucketDetailDisplayName, setBreadcrumbBucketDetailDisplayName] = + useState(null) return ( = ({ children }) => { setBreadcrumbUserProfileDisplayName, breadcrumbTeamDetailDisplayName, setBreadcrumbTeamDetailDisplayName, + breadcrumbBucketDetailDisplayName, + setBreadcrumbBucketDetailDisplayName, }} > {children} diff --git a/src/services/sharedBucketDetail.ts b/src/services/sharedBucketDetail.ts new file mode 100644 index 00000000..21c38367 --- /dev/null +++ b/src/services/sharedBucketDetail.ts @@ -0,0 +1,96 @@ +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 SharedBucketDetail { + [key: string]: Team | SharedBucket +} + +export interface SharedBucket { + short_name: string + bucket_name: string + teams: Team[] +} + +export interface Team { + uniform_name: string + display_name?: string + section_name: string +} + +const fetchTeamDetail = async (teamId: string): Promise => { + const teamUrl = new URL(`${TEAMS_URL}/${teamId}`) + + const selects = ['uniform_name', 'section_name'] + + teamUrl.searchParams.set('selects', selects.join(',')) + + try { + const teamDetail = await fetchAPIData(teamUrl.toString()) + if (!teamDetail) throw new ApiError(500, 'No json data returned') + + const flattenedTeamDetail = flattenEmbedded({ ...teamDetail }) + + return flattenedTeamDetail + } catch (error) { + if (error instanceof ApiError) { + console.error('Failed to fetch team detail:', error) + throw error + } else { + const apiError = new ApiError(500, 'An unexpected error occurred') + console.error('Failed to fetch team detail:', apiError) + throw apiError + } + } +} + +export const fetchSharedBucketDetailData = async (teamId: string, shortName: string): Promise => { + const sharedBucketUrl = new URL(`${TEAMS_URL}/${teamId}/shared/buckets/${shortName}`) + + const embeds = ['teams'] + const selects = ['teams.uniform_name', 'teams.display_name', 'teams.section_name'] + + sharedBucketUrl.searchParams.set('embed', embeds.join(',')) + sharedBucketUrl.searchParams.set('selects', selects.join(',')) + + try { + const sharedBucket = await fetchAPIData(sharedBucketUrl.toString()) + if (!sharedBucket) throw new ApiError(500, 'No json data returned') + + const flattenedSharedBuckets = flattenEmbedded({ ...sharedBucket }) + if (!flattenedSharedBuckets.teams) flattenedSharedBuckets.teams = [] + + return flattenedSharedBuckets + } catch (error) { + if (error instanceof ApiError) { + console.error('Failed to fetch shared buckets detail:', error) + throw error + } else { + const apiError = new ApiError(500, 'An unexpected error occurred') + console.error('Failed to fetch shared buckets detail:', apiError) + throw apiError + } + } +} + +export const getSharedBucketDetailData = async (teamId: string, shortName: string): Promise => { + try { + const [team, sharedBucket] = await Promise.all([ + fetchTeamDetail(teamId), + fetchSharedBucketDetailData(teamId, shortName), + ]) + + return { team, sharedBucket } + } catch (error) { + if (error instanceof ApiError) { + console.error('Failed to fetch shared bucket detail:', error) + throw error + } else { + const apiError = new ApiError(500, 'An unexpected error occurred while fetching shared bucket data') + console.error('Failed to fetch shared bucket detail:', apiError) + throw apiError + } + } +}