diff --git a/packages/tupaia-web-server/package.json b/packages/tupaia-web-server/package.json index b4d5ade3b8..3b7438620c 100644 --- a/packages/tupaia-web-server/package.json +++ b/packages/tupaia-web-server/package.json @@ -29,6 +29,7 @@ "@tupaia/api-client": "3.1.0", "@tupaia/database": "1.0.0", "@tupaia/server-boilerplate": "1.0.0", + "camelcase-keys": "^6.2.2", "dotenv": "^8.2.0", "express": "^4.16.2", "winston": "^3.2.1" diff --git a/packages/tupaia-web-server/src/routes/DashboardsRoute.ts b/packages/tupaia-web-server/src/routes/DashboardsRoute.ts index d98c19fac5..b9c4520d12 100644 --- a/packages/tupaia-web-server/src/routes/DashboardsRoute.ts +++ b/packages/tupaia-web-server/src/routes/DashboardsRoute.ts @@ -4,28 +4,40 @@ */ import { Request } from 'express'; +import camelcaseKeys from 'camelcase-keys'; import { Route } from '@tupaia/server-boilerplate'; -export type DashboardsRequest = Request< - any, - any, - any, - any ->; +export type DashboardsRequest = Request; export class DashboardsRoute extends Route { public async buildResponse() { const { query, ctx } = this.req; - const { organisationUnitCode, projectCode } = query; + const { entityCode, projectCode } = query; - const project = (await ctx.services.central.fetchResources('projects', { filter: { code: projectCode }, columns: JSON.stringify(['entity.code', 'entity_hierarchy.name']) }))[0]; - const baseEntity = await ctx.services.entity.getEntity(project['entity_hierarchy.name'], organisationUnitCode); - // TODO: Add a better getAncestors function to the EntityApi - const entities = await ctx.services.entity.getRelationshipsOfEntity(project['entity_hierarchy.name'], project['entity.code'], 'descendant', {}, {} , { filter: { type: baseEntity.type } }); - const dashboards = await ctx.services.central.fetchResources('dashboards', { filter: { root_entity_code: entities.ancestors }}); - return Promise.all(dashboards.map(async (dash: any) => ({ - ...dash, - items: await ctx.services.central.fetchResources(`dashboards/${dash.id}/dashboardRelations`) - }))); + const project = ( + await ctx.services.central.fetchResources('projects', { + filter: { code: projectCode }, + columns: JSON.stringify(['entity.code', 'entity_hierarchy.name']), + }) + )[0]; + const baseEntity = await ctx.services.entity.getEntity( + project['entity_hierarchy.name'], + entityCode, + ); + + const dashboards = await ctx.services.central.fetchResources('dashboards', { + filter: { root_entity_code: baseEntity.code }, + }); + + const response = await Promise.all( + dashboards.map(async (dash: any) => ({ + ...dash, + items: await ctx.services.central.fetchResources( + `dashboards/${dash.id}/dashboardRelations`, + ), + })), + ); + + return camelcaseKeys(response, { deep: true }); } } diff --git a/packages/tupaia-web/src/api/queries/index.ts b/packages/tupaia-web/src/api/queries/index.ts index 7edf2f38cb..510d8a7d41 100644 --- a/packages/tupaia-web/src/api/queries/index.ts +++ b/packages/tupaia-web/src/api/queries/index.ts @@ -9,3 +9,4 @@ export { useProjects } from './useProjects'; export { useUser } from './useUser'; export { useEntity } from './useEntity'; export { useEmailVerification } from './useEmailVerification'; +export { useDashboards } from './useDashboards'; diff --git a/packages/tupaia-web/src/api/queries/useDashboards.ts b/packages/tupaia-web/src/api/queries/useDashboards.ts new file mode 100644 index 0000000000..3015426da1 --- /dev/null +++ b/packages/tupaia-web/src/api/queries/useDashboards.ts @@ -0,0 +1,24 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ +import { useQuery } from 'react-query'; +import { Dashboard } from '@tupaia/types'; +import { get } from '../api'; + +type DashboardRecord = Dashboard & { + items: { childId: string }[]; +}; + +type DashboardsResponse = DashboardRecord[]; +export const useDashboards = (projectCode?: string, entityCode?: string) => { + return useQuery( + ['dashboards', projectCode, entityCode], + async (): Promise => { + return await get('dashboards', { + params: { entityCode, projectCode }, + }); + }, + { enabled: !!entityCode && !!projectCode }, + ); +}; diff --git a/packages/tupaia-web/src/constants/colors.ts b/packages/tupaia-web/src/constants/colors.ts index 6498756e29..732055f3b3 100644 --- a/packages/tupaia-web/src/constants/colors.ts +++ b/packages/tupaia-web/src/constants/colors.ts @@ -4,6 +4,7 @@ */ export const TRANSPARENT_BLACK = 'rgba(43, 45, 56, 0.94)'; + export const FORM_COLORS = { BORDER: '#d9d9d9', }; diff --git a/packages/tupaia-web/src/layout/Sidebar/Breadcrumbs.tsx b/packages/tupaia-web/src/layout/Sidebar/Breadcrumbs.tsx index cf7bd47bb6..812301a79a 100644 --- a/packages/tupaia-web/src/layout/Sidebar/Breadcrumbs.tsx +++ b/packages/tupaia-web/src/layout/Sidebar/Breadcrumbs.tsx @@ -13,4 +13,5 @@ export const Breadcrumbs = styled.div` max-width: 220px; background: #efefefaa; height: 15px; + z-index: 1; `; diff --git a/packages/tupaia-web/src/layout/Sidebar/DashboardMenu.tsx b/packages/tupaia-web/src/layout/Sidebar/DashboardMenu.tsx new file mode 100644 index 0000000000..47795c91b9 --- /dev/null +++ b/packages/tupaia-web/src/layout/Sidebar/DashboardMenu.tsx @@ -0,0 +1,82 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ +import React, { useState } from 'react'; +import { useLocation, Link, useParams } from 'react-router-dom'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; +import { ButtonBase, Menu, MenuItem } from '@material-ui/core'; +import { Dashboard } from '@tupaia/types'; +import styled from 'styled-components'; +import { useDashboards } from '../../api/queries'; +import { DashboardCode } from '../../types'; + +const MenuButton = styled(ButtonBase)` + display: flex; + justify-content: space-between; + align-items: center; + background-color: ${({ theme }) => theme.panel.secondaryBackground}; + width: 100%; + padding: 1rem; + font-size: 1rem; +`; + +interface DashboardMenuItemProps { + dashboardName: Dashboard['name']; + dashboardCode: DashboardCode; + onClose: () => void; +} + +const DashboardMenuItem = ({ dashboardName, dashboardCode, onClose }: DashboardMenuItemProps) => { + const location = useLocation(); + const { projectCode, entityCode } = useParams(); + + const link = { ...location, pathname: `/${projectCode}/${entityCode}/${dashboardCode}` }; + + return ( + + {dashboardName} + + ); +}; + +export const DashboardMenu = () => { + const [anchorEl, setAnchorEl] = useState(null); + const { projectCode, entityCode, '*': dashboardCode } = useParams(); + const { data: dashboards } = useDashboards(projectCode, entityCode); + + const handleClickListItem = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const selectedOption = dashboards?.find(({ code }) => code === dashboardCode); + + return ( + <> + + {selectedOption?.name} + + + + {dashboards?.map(({ name, code }) => ( + + ))} + + + ); +}; diff --git a/packages/tupaia-web/src/layout/Sidebar/ExpandButton.tsx b/packages/tupaia-web/src/layout/Sidebar/ExpandButton.tsx index f58be5633b..c1df46eb1f 100644 --- a/packages/tupaia-web/src/layout/Sidebar/ExpandButton.tsx +++ b/packages/tupaia-web/src/layout/Sidebar/ExpandButton.tsx @@ -7,7 +7,6 @@ import React from 'react'; import styled from 'styled-components'; import { KeyboardArrowLeft, KeyboardArrowRight } from '@material-ui/icons'; import { Button } from '@tupaia/ui-components'; -import { TRANSPARENT_BLACK } from '../../constants'; const SemiCircle = styled(Button)` position: absolute; @@ -16,7 +15,7 @@ const SemiCircle = styled(Button)` display: flex; justify-content: center; align-items: center; - background-color: ${TRANSPARENT_BLACK}; + background-color: ${({ theme }) => theme.panel.background}; min-height: 60px; min-width: 30px; border-top-left-radius: 60px; diff --git a/packages/tupaia-web/src/layout/Sidebar/Sidebar.tsx b/packages/tupaia-web/src/layout/Sidebar/Sidebar.tsx index 6150cc368c..65631784de 100644 --- a/packages/tupaia-web/src/layout/Sidebar/Sidebar.tsx +++ b/packages/tupaia-web/src/layout/Sidebar/Sidebar.tsx @@ -8,23 +8,22 @@ import styled from 'styled-components'; import { useParams } from 'react-router-dom'; import { Typography, Button } from '@material-ui/core'; import GetAppIcon from '@material-ui/icons/GetApp'; -import { TRANSPARENT_BLACK } from '../../constants'; import { ExpandButton } from './ExpandButton'; import { Photo } from './Photo'; import { Breadcrumbs } from './Breadcrumbs'; import { StaticMap } from './StaticMap'; -import { useEntity } from '../../api/queries'; +import { useDashboards, useEntity } from '../../api/queries'; +import { DashboardMenu } from './DashboardMenu'; const MAX_SIDEBAR_EXPANDED_WIDTH = 1000; const MAX_SIDEBAR_COLLAPSED_WIDTH = 500; const MIN_SIDEBAR_WIDTH = 335; -const PANEL_GREY = '#4a4b55'; const Panel = styled.div<{ $isExpanded: boolean; }>` position: relative; - background-color: ${TRANSPARENT_BLACK}; + background-color: ${({ theme }) => theme.panel.background}; transition: width 0.5s ease, max-width 0.5s ease; width: ${({ $isExpanded }) => ($isExpanded ? 55 : 30)}%; min-width: ${MIN_SIDEBAR_WIDTH}px; @@ -46,7 +45,7 @@ const TitleBar = styled.div` justify-content: space-between; align-items: center; padding: 1rem; - background-color: ${TRANSPARENT_BLACK}; + background-color: ${({ theme }) => theme.panel.background}; z-index: 1; border-bottom: 1px solid rgba(255, 255, 255, 0.2); `; @@ -64,18 +63,11 @@ const Title = styled(Typography)` line-height: 1.4; `; -const Dropdown = styled.div` - background: ${PANEL_GREY}; - width: 100%; - padding: 1rem; - font-size: 1rem; -`; - const ChartsContainer = styled.div<{ $isExpanded: boolean; }>` display: grid; - background-color: ${PANEL_GREY}; + background-color: ${({ theme }) => theme.panel.secondaryBackground}; grid-template-columns: repeat(auto-fill, minmax(${MIN_SIDEBAR_WIDTH}px, auto)); column-gap: 0.5rem; row-gap: 0.5rem; @@ -84,16 +76,21 @@ const ChartsContainer = styled.div<{ const Chart = styled.div` position: relative; - background-color: ${TRANSPARENT_BLACK}; + text-align: center; + background-color: ${({ theme }) => theme.panel.background}; // Use padding to maintain aspect ratio - padding-bottom: 80%; + padding: 1rem 1rem 75%; `; export const Sidebar = () => { + const { projectCode, entityCode, '*': dashboardCode } = useParams(); const [isExpanded, setIsExpanded] = useState(false); - const { entityCode } = useParams(); const { data: entityData } = useEntity(entityCode); const bounds = entityData?.location?.bounds; + + const { data: dashboardData } = useDashboards(projectCode, entityCode); + const activeDashboard = dashboardData?.find(dashboard => dashboard.code === dashboardCode); + const toggleExpanded = () => { setIsExpanded(!isExpanded); }; @@ -109,21 +106,14 @@ export const Sidebar = () => { )} - Northern + {entityData?.name} }>Export - General + - - - - - - - - - - + {activeDashboard?.items.map(({ childId }) => { + return DashboardId: {childId}; + })} diff --git a/packages/tupaia-web/src/layout/UserMenu/MenuList.tsx b/packages/tupaia-web/src/layout/UserMenu/MenuList.tsx index 3218fdd234..4d71c93f59 100644 --- a/packages/tupaia-web/src/layout/UserMenu/MenuList.tsx +++ b/packages/tupaia-web/src/layout/UserMenu/MenuList.tsx @@ -6,7 +6,7 @@ import React, { ReactNode } from 'react'; import styled from 'styled-components'; import { Button, ListItem, ListItemProps } from '@material-ui/core'; -import { RouterLink } from '../../components/RouterButton'; +import { RouterLink } from '../../components'; /** * Menulist is a component that displays a list of menu items for the hamburger menu diff --git a/packages/tupaia-web/src/pages/Login.tsx b/packages/tupaia-web/src/pages/Login.tsx index 43afc24c4f..31a3f5008f 100644 --- a/packages/tupaia-web/src/pages/Login.tsx +++ b/packages/tupaia-web/src/pages/Login.tsx @@ -64,7 +64,8 @@ export const Login = () => { Log in - Don't have an account? Register here + Don't have an account?{' '} + Register here diff --git a/packages/tupaia-web/src/theme/theme.ts b/packages/tupaia-web/src/theme/theme.ts index cddae63c2b..27db725855 100644 --- a/packages/tupaia-web/src/theme/theme.ts +++ b/packages/tupaia-web/src/theme/theme.ts @@ -1,4 +1,5 @@ import { createMuiTheme } from '@material-ui/core'; +import { TRANSPARENT_BLACK } from '../constants'; export const theme = createMuiTheme( { @@ -19,6 +20,10 @@ export const theme = createMuiTheme( }, }, { + panel: { + background: TRANSPARENT_BLACK, + secondaryBackground: '#4a4b55', + }, projectCard: { background: '#2e2f33', fallBack: '#EFEFF0', diff --git a/packages/tupaia-web/src/types/types.d.ts b/packages/tupaia-web/src/types/types.d.ts index 9998dd52cb..f5a519a987 100644 --- a/packages/tupaia-web/src/types/types.d.ts +++ b/packages/tupaia-web/src/types/types.d.ts @@ -1,4 +1,4 @@ -import { LandingPage, Project } from '@tupaia/types'; +import { LandingPage, Project, Entity, Dashboard } from '@tupaia/types'; import { KeysToCamelCase } from './helpers'; export type SingleProject = KeysToCamelCase & { @@ -13,3 +13,15 @@ export type SingleProject = KeysToCamelCase & { export type SingleLandingPage = KeysToCamelCase> & { projects: SingleProject[]; }; + +export type ProjectCode = Project['code']; + +export type EntityCode = Entity['code']; + +export type DashboardCode = Dashboard['code']; + +export type TupaiaUrlParams = { + projectCode?: ProjectCode; + entityCode?: EntityCode; + dashboardCode?: DashboardCode; +}; diff --git a/yarn.lock b/yarn.lock index 2ad83f09cb..27d1a702db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10070,6 +10070,7 @@ __metadata: "@tupaia/api-client": 3.1.0 "@tupaia/database": 1.0.0 "@tupaia/server-boilerplate": 1.0.0 + camelcase-keys: ^6.2.2 dotenv: ^8.2.0 express: ^4.16.2 winston: ^3.2.1