Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: GTFS Schedule Analytics UI #653

Merged
merged 10 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions web-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mui/icons-material": "^5.14.9",
"@mui/material": "^5.14.9",
"@mui/icons-material": "^5.16.4",
"@mui/material": "^5.16.4",
"@mui/x-date-pickers": "^7.11.0",
"@mui/x-tree-view": "^6.17.0",
"@reduxjs/toolkit": "^1.9.6",
"@turf/center": "^6.5.0",
"@types/i18next": "^13.0.0",
"@types/leaflet": "^1.9.12",
"axios": "^1.7.2",
cka-y marked this conversation as resolved.
Show resolved Hide resolved
"country-code-emoji": "^2.3.0",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
Expand All @@ -26,6 +28,9 @@
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"leaflet": "^1.9.4",
"material-react-table": "^2.13.0",
"mui-datatables": "^4.3.0",
"mui-nested-menu": "^3.4.0",
"openapi-fetch": "^0.9.3",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
Expand All @@ -38,6 +43,7 @@
"react-redux": "^8.1.3",
"react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
"recharts": "^2.12.7",
"redux-persist": "^6.0.0",
"redux-saga": "^1.2.3",
"typeface-muli": "^1.1.13",
Expand Down Expand Up @@ -95,6 +101,7 @@
"@types/cypress": "^1.1.3",
"@types/jest": "^29.5.12",
"@types/material-ui": "^0.21.12",
"@types/mui-datatables": "^4.3.12",
"@types/node": "^20.8.10",
"@types/react": "^18.2.25",
"@types/react-dom": "^18.2.7",
Expand Down
22 changes: 13 additions & 9 deletions web-app/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import i18n from '../i18n';
import { Suspense, useEffect, useState } from 'react';
import { I18nextProvider } from 'react-i18next';
import { app } from '../firebase';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';

function App(): React.ReactElement {
require('typeface-muli'); // Load font
Expand All @@ -33,15 +35,17 @@ function App(): React.ReactElement {
<RemoteConfigProvider>
<I18nextProvider i18n={i18n}>
<Suspense>
<div id='app-main-container'>
<AppSpinner>
<BrowserRouter>
<Header />
{isAppReady ? <AppRouter /> : null}
</BrowserRouter>
</AppSpinner>
<Footer />
</div>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<div id='app-main-container'>
<AppSpinner>
<BrowserRouter>
<Header />
{isAppReady ? <AppRouter /> : null}
</BrowserRouter>
</AppSpinner>
<Footer />
</div>
</LocalizationProvider>
</Suspense>
</I18nextProvider>
</RemoteConfigProvider>
Expand Down
128 changes: 124 additions & 4 deletions web-app/src/app/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,18 @@ import {
import type NavigationItem from '../interface/Navigation';
import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { selectIsAuthenticated } from '../store/selectors';
import { selectIsAuthenticated, selectUserEmail } from '../store/selectors';
import LogoutConfirmModal from './LogoutConfirmModal';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { TreeView } from '@mui/x-tree-view/TreeView';
import { TreeItem } from '@mui/x-tree-view/TreeItem';
import { OpenInNew } from '@mui/icons-material';
import { BikeScooterOutlined, OpenInNew } from '@mui/icons-material';
import '../styles/Header.css';
import { useRemoteConfig } from '../context/RemoteConfigProvider';
import i18n from '../../i18n';
import { NestedMenuItem } from 'mui-nested-menu';
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';

const drawerWidth = 240;
const websiteTile = 'Mobility Database';
Expand Down Expand Up @@ -95,6 +97,38 @@ const DrawerContent: React.FC<{
</ListItem>
))}
<Divider sx={{ mt: 2, mb: 2 }} />
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ textAlign: 'left' }}
>
<TreeItem nodeId='1' label='GTFS Metrics' sx={{ color: '#3959fa' }}>
<TreeItem
nodeId='2'
label='Feeds'
sx={{ color: '#7c7c7c', cursor: 'pointer' }}
onClick={() => {
onNavigationClick('/metrics/gtfs/feeds');
}}
/>
<TreeItem
nodeId='3'
label='Notices'
sx={{ color: '#7c7c7c', cursor: 'pointer' }}
onClick={() => {
onNavigationClick('/metrics/gtfs/notices');
}}
/>
<TreeItem
nodeId='4'
label='Features'
sx={{ color: '#7c7c7c', cursor: 'pointer' }}
onClick={() => {
onNavigationClick('/metrics/gtfs/features');
}}
/>
</TreeItem>
</TreeView>
{isAuthenticated ? (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
Expand Down Expand Up @@ -166,6 +200,7 @@ export default function DrawerAppBar(): React.ReactElement {

const navigateTo = useNavigate();
const isAuthenticated = useSelector(selectIsAuthenticated);
const userEmail = useSelector(selectUserEmail);

const handleDrawerToggle = (): void => {
setMobileOpen((prevState) => !prevState);
Expand Down Expand Up @@ -199,7 +234,7 @@ export default function DrawerAppBar(): React.ReactElement {
setAnchorEl(null);
};

const handleMenuItemClick = (item: NavigationItem): void => {
const handleMenuItemClick = (item: NavigationItem | string): void => {
handleMenuClose();
handleNavigation(item);
};
Expand Down Expand Up @@ -260,20 +295,104 @@ export default function DrawerAppBar(): React.ReactElement {
{item.title}
</Button>
))}
{/* Allow users with mobilitydata.org email to access metrics */}
{config.enableMetrics ||
(userEmail?.endsWith('mobilitydata.org') === true && (
<>
<Button
aria-controls='analytics-menu'
aria-haspopup='true'
endIcon={<ArrowDropDownIcon />}
onClick={handleMenuOpen}
sx={{ color: 'black' }}
id='analytics-button-menu'
>
Metrics
</Button>
<Menu
id='analytics-menu'
anchorEl={anchorEl}
open={
anchorEl !== null &&
anchorEl.id === 'analytics-button-menu'
}
onClose={handleMenuClose}
>
<NestedMenuItem
label='GTFS'
parentMenuOpen={Boolean(anchorEl)}
leftIcon={<DirectionsBusIcon />}
>
<MenuItem
onClick={() => {
handleMenuItemClick('/metrics/gtfs/feeds');
}}
>
Feeds
</MenuItem>
<MenuItem
onClick={() => {
handleMenuItemClick('/metrics/gtfs/notices');
}}
>
Notices
</MenuItem>
<MenuItem
onClick={() => {
handleMenuItemClick('/metrics/gtfs/features');
}}
>
Features
</MenuItem>
</NestedMenuItem>
<NestedMenuItem
label='GBFS'
parentMenuOpen={Boolean(anchorEl)}
leftIcon={<BikeScooterOutlined />}
>
<MenuItem
onClick={() => {
handleMenuItemClick('/metrics/gbfs/feeds');
}}
>
Feeds
</MenuItem>
<MenuItem
onClick={() => {
handleMenuItemClick('/metrics/gbfs/notices');
}}
>
Notices
</MenuItem>
<MenuItem
onClick={() => {
handleMenuItemClick('/metrics/gbfs/versions');
}}
>
Versions
</MenuItem>
</NestedMenuItem>
</Menu>
</>
))}

{isAuthenticated ? (
<>
<Button
aria-controls='account-menu'
aria-haspopup='true'
onClick={handleMenuOpen}
endIcon={<ArrowDropDownIcon />}
id='account-button-menu'
>
Account
</Button>
<Menu
id='account-menu'
anchorEl={anchorEl}
open={Boolean(anchorEl)}
open={
anchorEl !== null && anchorEl.id === 'account-button-menu'
}
onClose={handleMenuClose}
>
<MenuItem
Expand Down Expand Up @@ -304,6 +423,7 @@ export default function DrawerAppBar(): React.ReactElement {
onChange={(lang) => {
void i18n.changeLanguage(lang.target.value);
}}
variant='standard'
>
<MenuItem value={'en'}>EN</MenuItem>
<MenuItem value={'fr'}>FR</MenuItem>
Expand Down
2 changes: 2 additions & 0 deletions web-app/src/app/constants/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const MOBILITY_DATA_LINKS = {
github: 'https://github.com/MobilityData/mobility-database-catalogs',
};

export const WEB_VALIDATOR_LINK = 'https://gtfs-validator.mobilitydata.org';

export function buildNavigationItems(
featureFlags: RemoteConfigValues,
): NavigationItem[] {
Expand Down
11 changes: 11 additions & 0 deletions web-app/src/app/interface/RemoteConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export interface RemoteConfigValues extends FirebaseDefaultConfig {
* false: renders the legacy feed submission page based in the Contribute.tsx
*/
enableFeedSubmissionStepper: boolean;
/** Enable Metrics view
* Values:
* true: renders the metrics view
* false: hides the metrics view
*/
enableMetrics: boolean;
/** GTFS metrics' bucket endpoint */
gtfsMetricsBucketEndpoint: string;
}

// Add default values for remote config here
Expand All @@ -21,6 +29,9 @@ export const defaultRemoteConfigValues: RemoteConfigValues = {
enableFeedsPage: false,
enableLanguageToggle: false,
enableFeedSubmissionStepper: false,
enableMetrics: false,
gtfsMetricsBucketEndpoint:
'https://storage.googleapis.com/mobilitydata-gtfs-analytics-dev',
};

remoteConfig.defaultConfig = defaultRemoteConfigValues;
9 changes: 9 additions & 0 deletions web-app/src/app/router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import { logout } from '../store/profile-reducer';
import FeedSubmission from '../screens/FeedSubmission';
import FeedSubmissionFAQ from '../screens/FeedSubmissionFAQ';
import FeedSubmitted from '../screens/FeedSubmitted';
import GTFSFeedAnalytics from '../screens/Analytics/GTFSFeedAnalytics';
import GTFSNoticeAnalytics from '../screens/Analytics/GTFSNoticeAnalytics';
import GTFSFeatureAnalytics from '../screens/Analytics/GTFSFeatureAnalytics';

export const AppRouter: React.FC = () => {
const navigateTo = useNavigate();
Expand Down Expand Up @@ -89,6 +92,12 @@ export const AppRouter: React.FC = () => {
<Route path='contribute-faq' element={<FeedSubmissionFAQ />} />
<Route path='privacy-policy' element={<PrivacyPolicy />} />
<Route path='terms-and-conditions' element={<TermsAndConditions />} />
<Route path='metrics/gtfs'>
davidgamez marked this conversation as resolved.
Show resolved Hide resolved
<Route index element={<GTFSFeedAnalytics />} />
<Route path='feeds/*' element={<GTFSFeedAnalytics />} />
<Route path='notices/*' element={<GTFSNoticeAnalytics />} />
<Route path='features/*' element={<GTFSFeatureAnalytics />} />
</Route>
</Routes>
);
};
Expand Down
Loading