Skip to content

Commit fabc85c

Browse files
Feat: gbfs feed detail page + gbfs in search page (#1140)
* gbfs endpoint integration * gbfs components * gbfs filling real values + search * gbfs features filter and sort * gbfs search bug fixes * translations * gbfs version tests * gbfs auto discover link to external * wording adjustments
1 parent ab9ab02 commit fabc85c

25 files changed

+1020
-227
lines changed

web-app/public/locales/en/feeds.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
},
4646
"seeFullList": "See full list",
4747
"hideFullList": "Hide full list",
48+
"producer": "Producer",
4849
"producerDownloadUrl": "Producer download URL",
4950
"copyDownloadUrl": "Copy download URL",
5051
"producerUrlCopied": "Producer url copied to clipboard",
@@ -141,5 +142,7 @@
141142
"downloadReport": "Download the dataset for this feed",
142143
"viewReport": "View Validation Report",
143144
"viewJsonReport": "View Validation Report in JSON format"
144-
}
145+
},
146+
"viewRealtimeVisualization": "View real-time visualization",
147+
"versions": "Versions"
145148
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"versions": "GBFS Versions",
3+
"feedUrl": "Feed URL",
4+
"feedUrlCopied": "Feed URL Copied",
5+
"runValidationReport": "Run Validation Report",
6+
"openFeedUrl": "Open Feed URL",
7+
"features": {
8+
"gbfs": "GBFS",
9+
"system_regions": "System Regions",
10+
"system_information": "System Information",
11+
"geofencing_zones": "Geofencing Zones",
12+
"system_pricing_plans": "System Pricing Plans",
13+
"vehicle_types": "Vehicle Types",
14+
"vehicle_status": "Vehicle Status",
15+
"station_status": "Station Status",
16+
"system_calendar": "System Calendar",
17+
"system_hours": "System Hours",
18+
"gbfs_versions": "GBFS Versions",
19+
"station_information": "Station Information",
20+
"system_alerts": "System Alerts",
21+
"free_bike_status": "Free Bike Status",
22+
"nearby_stations": "Nearby Stations"
23+
}
24+
}

web-app/src/app/components/ContentBox.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Box, Typography, useTheme, type SxProps } from '@mui/material';
33

44
export interface ContentBoxProps {
55
title: string;
6-
width: Record<string, string>;
6+
width?: Record<string, string>;
77
outlineColor: string;
88
padding?: Partial<SxProps>;
99
margin?: string | number;
@@ -17,7 +17,7 @@ export const ContentBox = (
1717
const theme = useTheme();
1818
return (
1919
<Box
20-
width={props.width}
20+
width={props.width ?? { xs: '100%', sm: '100%', md: '100%' }}
2121
sx={{
2222
background: theme.palette.background.default,
2323
color: theme.palette.text.primary,

web-app/src/app/components/CoveredAreaMap.tsx

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ToggleButton,
66
Tooltip,
77
Skeleton,
8+
Button,
89
} from '@mui/material';
910
import MapIcon from '@mui/icons-material/Map';
1011
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
@@ -16,10 +17,13 @@ import { Map } from './Map';
1617
import { useTranslation } from 'react-i18next';
1718
import type { LatLngExpression } from 'leaflet';
1819
import { useTheme } from '@mui/material/styles';
20+
import { type GBFSFeedType, type AllFeedType } from '../services/feeds/utils';
21+
import { OpenInNew } from '@mui/icons-material';
1922

2023
interface CoveredAreaMapProps {
2124
boundingBox?: LatLngExpression[];
2225
latestDataset?: { hosted_url?: string };
26+
feed: AllFeedType;
2327
}
2428

2529
export const fetchGeoJson = async (
@@ -40,6 +44,7 @@ export const fetchGeoJson = async (
4044
const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
4145
boundingBox,
4246
latestDataset,
47+
feed,
4348
}) => {
4449
const { t } = useTranslation('feeds');
4550
const theme = useTheme();
@@ -82,45 +87,67 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
8287
if (newView !== null) setView(newView);
8388
};
8489

90+
const getGbfsLatestVersionVisualizationUrl = (feed: GBFSFeedType): string => {
91+
// TODO: Redo logic when versions all have the auto discovery
92+
return `https://gbfs-validator.mobilitydata.org/visualization?url=${feed?.source_info?.producer_url}`;
93+
};
94+
8595
return (
8696
<ContentBox
8797
sx={{
8898
flexGrow: 1,
8999
display: 'flex',
90100
flexDirection: 'column',
101+
maxHeight: {
102+
xs: '100%',
103+
md: '70vh',
104+
},
91105
}}
92106
title={t('coveredAreaTitle') + ' - ' + t(view)}
93107
width={{ xs: '100%' }}
94108
outlineColor={theme.palette.primary.dark}
95109
padding={2}
96110
action={
97-
<ToggleButtonGroup
98-
value={view}
99-
color='primary'
100-
exclusive
101-
aria-label='map view selection'
102-
onChange={handleViewChange}
103-
>
104-
<Tooltip title={t('detailedCoveredAreaViewTooltip')}>
105-
<ToggleButton
106-
value='detailedCoveredAreaView'
107-
disabled={
108-
geoJsonLoading || geoJsonError || boundingBox === undefined
109-
}
110-
aria-label='Detailed Covered Area View'
111+
<>
112+
{feed?.data_type === 'gbfs' ? (
113+
<Button
114+
href={getGbfsLatestVersionVisualizationUrl(feed as GBFSFeedType)}
115+
target='_blank'
116+
rel='noreferrer'
117+
endIcon={<OpenInNew />}
111118
>
112-
<TravelExploreIcon />
113-
</ToggleButton>
114-
</Tooltip>
115-
<Tooltip title={t('boundingBoxViewTooltip')}>
116-
<ToggleButton
117-
value='boundingBoxView'
118-
aria-label='Bounding Box View'
119+
{t('viewRealtimeVisualization')}
120+
</Button>
121+
) : (
122+
<ToggleButtonGroup
123+
value={view}
124+
color='primary'
125+
exclusive
126+
aria-label='map view selection'
127+
onChange={handleViewChange}
119128
>
120-
<MapIcon />
121-
</ToggleButton>
122-
</Tooltip>
123-
</ToggleButtonGroup>
129+
<Tooltip title={t('detailedCoveredAreaViewTooltip')}>
130+
<ToggleButton
131+
value='detailedCoveredAreaView'
132+
disabled={
133+
geoJsonLoading || geoJsonError || boundingBox === undefined
134+
}
135+
aria-label='Detailed Covered Area View'
136+
>
137+
<TravelExploreIcon />
138+
</ToggleButton>
139+
</Tooltip>
140+
<Tooltip title={t('boundingBoxViewTooltip')}>
141+
<ToggleButton
142+
value='boundingBoxView'
143+
aria-label='Bounding Box View'
144+
>
145+
<MapIcon />
146+
</ToggleButton>
147+
</Tooltip>
148+
</ToggleButtonGroup>
149+
)}
150+
</>
124151
}
125152
>
126153
{boundingBox === undefined && view === 'boundingBoxView' && (

web-app/src/app/interface/RemoteConfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface RemoteConfigValues extends FirebaseDefaultConfig {
3131
enableFeatureFilterSearch: boolean;
3232
enableIsOfficialFilterSearch: boolean;
3333
enableFeedStatusBadge: boolean;
34+
enableGbfsInSearchPage: boolean;
3435
}
3536

3637
const featureByPassDefault: BypassConfig = {
@@ -52,6 +53,7 @@ export const defaultRemoteConfigValues: RemoteConfigValues = {
5253
enableFeatureFilterSearch: false,
5354
enableIsOfficialFilterSearch: false,
5455
enableFeedStatusBadge: false,
56+
enableGbfsInSearchPage: false,
5557
};
5658

5759
remoteConfig.defaultConfig = defaultRemoteConfigValues;

web-app/src/app/screens/Feed/Feed.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
generatePageTitle,
1010
generateDescriptionMetaTag,
1111
} from './Feed.functions';
12-
import FeedTitle from './FeedTitle';
12+
import FeedTitle from './components/FeedTitle';
1313

1414
const mockFeed: GTFSFeedType = {
1515
id: 'mdb-x',

web-app/src/app/screens/Feed/Feed.styles.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { type SxProps, type Theme } from '@mui/material';
1+
import { Box, styled, type SxProps, type Theme } from '@mui/material';
22

33
export const feedDetailContentContainerStyle = (props: {
44
theme: Theme;
5-
isGtfsSchedule: boolean;
5+
isGtfsRT: boolean;
66
}): SxProps => {
77
return {
88
width: '100%',
99
display: 'flex',
1010
flexDirection: {
11-
xs: props.isGtfsSchedule ? 'column-reverse' : 'column',
12-
md: props.isGtfsSchedule ? 'row-reverse' : 'row',
11+
xs: props.isGtfsRT ? 'column' : 'column-reverse',
12+
md: props.isGtfsRT ? 'row' : 'row-reverse',
1313
},
1414
gap: 3,
1515
flexWrap: 'nowrap',
@@ -27,9 +27,53 @@ export const ctaContainerStyle: SxProps<Theme> = (theme) => ({
2727
pt: 3,
2828
});
2929

30+
export const featureChipsStyle: SxProps<Theme> = (theme) => ({
31+
color: theme.palette.primary.contrastText,
32+
backgroundColor: theme.palette.primary.dark,
33+
border: `2px solid transparent`,
34+
':hover': {
35+
opacity: 0.95,
36+
},
37+
});
38+
3039
export const mapBoxPositionStyle = {
3140
width: 'calc(100% + 32px)',
3241
flexGrow: 1,
3342
mb: '-16px',
3443
mx: '-16px',
3544
};
45+
46+
export const boxElementStyle: SxProps = {
47+
width: '100%',
48+
mt: 2,
49+
mb: 1,
50+
};
51+
52+
export const boxElementStyleTransitProvider: SxProps = {
53+
width: '100%',
54+
mt: 2,
55+
borderBottom: 'none',
56+
};
57+
58+
export const boxElementStyleProducerURL: SxProps = {
59+
width: '100%',
60+
mb: 1,
61+
};
62+
63+
export const StyledTitleContainer = styled(Box)(({ theme }) => ({
64+
display: 'flex',
65+
gap: theme.spacing(1),
66+
marginBottom: '4px',
67+
marginTop: theme.spacing(3),
68+
alignItems: 'center',
69+
}));
70+
71+
export const ResponsiveListItem = styled('li')(({ theme }) => ({
72+
width: '100%',
73+
margin: '5px 0',
74+
fontWeight: 'normal',
75+
fontSize: '16px',
76+
[theme.breakpoints.up('lg')]: {
77+
width: 'calc(50% - 15px)',
78+
},
79+
}));

web-app/src/app/screens/Feed/AssociatedFeeds.tsx renamed to web-app/src/app/screens/Feed/components/AssociatedFeeds.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { ContentBox } from '../../components/ContentBox';
2+
import { ContentBox } from '../../../components/ContentBox';
33
import {
44
Box,
55
TableBody,
@@ -13,7 +13,7 @@ import {
1313
type GTFSFeedType,
1414
type AllFeedType,
1515
type GTFSRTFeedType,
16-
} from '../../services/feeds/utils';
16+
} from '../../../services/feeds/utils';
1717
import { Link } from 'react-router-dom';
1818

1919
export interface AssociatedFeedsProps {

web-app/src/app/screens/Feed/DataQualitySummary.tsx renamed to web-app/src/app/screens/Feed/components/DataQualitySummary.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as React from 'react';
22
import { Box, Chip } from '@mui/material';
33
import { CheckCircle, ReportOutlined } from '@mui/icons-material';
4-
import { type components } from '../../services/feeds/types';
4+
import { type components } from '../../../services/feeds/types';
55
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
6-
import { WarningContentBox } from '../../components/WarningContentBox';
6+
import { WarningContentBox } from '../../../components/WarningContentBox';
77
import { useTranslation } from 'react-i18next';
8-
import { FeedStatusChip } from '../../components/FeedStatus';
9-
import { useRemoteConfig } from '../../context/RemoteConfigProvider';
10-
import OfficialChip from '../../components/OfficialChip';
8+
import { FeedStatusChip } from '../../../components/FeedStatus';
9+
import { useRemoteConfig } from '../../../context/RemoteConfigProvider';
10+
import OfficialChip from '../../../components/OfficialChip';
1111

1212
export interface DataQualitySummaryProps {
1313
feedStatus: components['schemas']['Feed']['status'];
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Box, Button, Typography } from '@mui/material';
2+
import { type AllFeedType } from '../../../services/feeds/utils';
3+
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
4+
import LockIcon from '@mui/icons-material/Lock';
5+
import { useTranslation } from 'react-i18next';
6+
import { boxElementStyle, StyledTitleContainer } from '../Feed.styles';
7+
8+
interface FeedAuthenticationSummaryInfoProps {
9+
feed: AllFeedType;
10+
}
11+
12+
export default function FeedAuthenticationSummaryInfo({
13+
feed,
14+
}: FeedAuthenticationSummaryInfoProps): React.ReactElement {
15+
const { t } = useTranslation('feeds');
16+
17+
const hasAuthenticationInfo =
18+
feed?.source_info?.authentication_info_url != undefined &&
19+
feed?.source_info.authentication_info_url.trim() !== '';
20+
return (
21+
<>
22+
{feed?.source_info?.authentication_type !== 0 && (
23+
<Box sx={boxElementStyle}>
24+
<StyledTitleContainer>
25+
<LockIcon></LockIcon>
26+
<Typography variant='subtitle1' sx={{ fontWeight: 'bold' }}>
27+
{t('authenticationType')}
28+
</Typography>
29+
<Typography data-testid='data-type'>
30+
{feed?.source_info?.authentication_type === 1 &&
31+
t('common:apiKey')}
32+
{feed?.source_info?.authentication_type === 2 &&
33+
t('common:httpHeader')}
34+
</Typography>
35+
</StyledTitleContainer>
36+
</Box>
37+
)}
38+
39+
{hasAuthenticationInfo &&
40+
feed?.source_info?.authentication_info_url != undefined && (
41+
<Button
42+
disableElevation
43+
variant='outlined'
44+
href={feed?.source_info?.authentication_info_url}
45+
target='_blank'
46+
rel='noreferrer'
47+
sx={{ marginRight: 2 }}
48+
endIcon={<OpenInNewIcon />}
49+
>
50+
{t('registerToDownloadFeed')}
51+
</Button>
52+
)}
53+
</>
54+
);
55+
}

0 commit comments

Comments
 (0)