Skip to content

Commit

Permalink
[WIP] Regional dashboards wip (#227)
Browse files Browse the repository at this point in the history
* WIP Test Release oif the National Dashboards - this is restricted to only those with the forecast:view role and is being merged to test functionality in production with real data. Not ready for consumtpion by the wider user base!

---------

Co-authored-by: jackson <Jackson Hyde>
Co-authored-by: Rich Birch <[email protected]>
  • Loading branch information
jacksonhyde and richbirch authored Mar 6, 2024
1 parent fce189e commit 44a5627
Show file tree
Hide file tree
Showing 17 changed files with 1,006 additions and 12 deletions.
3 changes: 3 additions & 0 deletions react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"@types/react-dom": "^18.2.14",
"@types/validator": "^13.11.7",
"axios": "^0.21.1",
"chart.js": "^4.4.1",
"chartjs-adapter-moment": "^1.0.1",
"date-fns": "^2.29.1",
"formik": "^2.4.5",
"lodash": "^4.17.21",
Expand All @@ -30,6 +32,7 @@
"momentjs": "^2.0.0",
"notistack": "^3.0.1",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
"react-redux": "^8.1.3",
Expand Down
2 changes: 2 additions & 0 deletions react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {FeedbackList} from "./components/feedback/FeedbackList";
import DownloadManager from './components/downloadmanager/DownloadManager';
import {ExportConfig} from "./components/ExportConfig";
import {HealthChecks} from "./components/healthchecks/HealthChecks";
import RegionalDashboard from './components/regionalpressure/RegionalDashboard';

const StyledDiv = styled('div')(() => ({
textAlign: 'center',
Expand Down Expand Up @@ -97,6 +98,7 @@ export const App = () => {
<Route path={"/access-requests"} element={<AccessRequests/>}/>
<Route path={"/users"} element={<UsersList/>}/>
<Route path={"/download"} element={<DownloadManager config={config.values} user={user.profile} />} />
<Route path={"/national-pressure"} element={<RegionalDashboard config={config.values} user={user.profile} />} />
<Route path={"/alerts"} element={<Alerts regions={config.values.portsByRegion} user={user.profile}/>}/>
<Route path={"/region/:regionName"} element={<RegionPage user={user.profile} config={config.values}/>}/>
<Route path={"/feature-guides"}>
Expand Down
1 change: 1 addition & 0 deletions react/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default function Navigation(props: IProps) {
{label: 'Alert notices', link: '/alerts', roles: ['manage-users']},
{label: 'Drop-in sessions', link: '/drop-in-sessions', roles: ['manage-users']},
{label: 'Download Manager', link: '/download', roles: ['download-manager']},
{label: 'National Dashboard', link: '/national-pressure', roles: ['forecast:view']},
{label: 'Export Config', link: '/export-config', roles: ['manage-users']},
{label: 'Feature guides', link: '/feature-guides', roles: ['manage-users']},
{label: 'Health checks', link: '/health-checks', roles: ['health-checks:edit']},
Expand Down
149 changes: 149 additions & 0 deletions react/src/components/regionalpressure/ RegionalPressureChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import * as React from 'react';
import {connect} from 'react-redux';
import {
Alert,
Card,
CardContent,
CardHeader,
Button,
Stack,
useTheme,
} from "@mui/material";
import { CheckCircle } from '@mui/icons-material';
import ErrorIcon from '@mui/icons-material/Error';
import {RootState} from '../../store/redux';
import drtTheme from '../../drtTheme';
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
} from 'chart.js';
import { Radar } from 'react-chartjs-2';

ChartJS.register(
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend
);

interface RegionalPressureChartProps {
regionName: string;
portCodes: string[];
portTotals: {
[key: string]: number
};
historicPortTotals: {
[key: string]: number
};
onMoreInfo: (region :string) => void
}

const doesExceed = (forecast: number, historic: number): boolean => {
return forecast > historic;
}

const RegionalPressureChart = ({regionName, portCodes, portTotals, historicPortTotals, onMoreInfo}: RegionalPressureChartProps) => {
const theme = useTheme();

const forecasts = [...portCodes].map(portCode => portTotals[portCode]);
const historics = [...portCodes].map(portCode => historicPortTotals[portCode]);

const chartData = {
labels: portCodes,
datasets: [
{
label: 'Forecasted PAX arrivals',
data: forecasts,
backgroundColor: 'rgba(0, 94, 165, 0.2)',
borderColor: drtTheme.palette.primary.main,
borderDash: [5, 5],
pointStyle: 'rectRot',
borderWidth: 1,
},
{
label: 'Historic PAX average',
data: historics,
backgroundColor: 'transparent',
borderColor: '#547a00',
pointStyle: 'circle',
pointBackgroundColor: '#547a00',
borderDash: [0,0],
borderWidth: 1,
},
],
};

const exceededForecasts = forecasts.map((forecast, index) => {return (doesExceed(forecast!, historics[index]!))});
const exceededCount = exceededForecasts.filter(forecast => forecast).length;

const chartOptions = {
layout: {
padding: 0
},
plugins: {
legend: {
labels: {
usePointStyle: true,
}
}
},
scales: {
r: {
pointLabels: {
callback: (label: string, index: number): string | number | string[] | number[] => {
return doesExceed(forecasts[index]!, historics[index]!) ? `⚠ ${label}` : `${label}`;
},
font: {
weight: (context: any) => {
return doesExceed(forecasts[context.index]!, historics[context.index]!) ? 'bold' : 'normal'
}
},
color: (context: any) => {
return doesExceed(forecasts[context.index]!, historics[context.index]!) ? theme.palette.warning.main : 'black';
},
},
}
}
}

return (
<Card variant='outlined'>
<CardHeader title={regionName} />
<CardContent>
<Stack sx={{ width: '100%' }} spacing={2}>
<Radar data={chartData} options={chartOptions} />
<Button onClick={() => onMoreInfo(regionName)} fullWidth variant='contained'>More Info</Button>

{ exceededCount > 0 ?
<Alert icon={<ErrorIcon fontSize="inherit" />} severity="info">
{`PAX arrivals exceeds historic average across ${exceededCount} airports`}
</Alert>
:
<Alert icon={<CheckCircle fontSize="inherit" />} severity="success">
Pax arrivals do not exceed historic average at any airport
</Alert>
}
</Stack>
</CardContent>
</Card>
)

}


const mapState = (state: RootState) => {
return {
portTotals: state.pressureDashboard?.portTotals,
historicPortTotals: state.pressureDashboard?.historicPortTotals,
};
}


export default connect(mapState)(RegionalPressureChart);
157 changes: 157 additions & 0 deletions react/src/components/regionalpressure/RegionalDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import * as React from 'react';
import {connect, MapDispatchToProps} from 'react-redux';
import {
Box,
Grid,
FormControl,
FormControlLabel,
FormLabel,
RadioGroup,
Radio,
TextField,
} from "@mui/material";
import {DatePicker} from '@mui/x-date-pickers/DatePicker';
import {UserProfile} from "../../model/User";
import {ConfigValues, PortRegion} from "../../model/Config";
import moment, {Moment} from 'moment';
import {RootState} from '../../store/redux';
import { FormError } from '../../services/ValidationService';

import { requestPaxTotals } from './regionalPressureSagas';
import RegionalPressureViewSwitch from './RegionalPressureViewSwitch';

interface RegionalPressureDashboardProps {
user: UserProfile;
config: ConfigValues;
errors: FormError[];
type?: string;
start?: string;
end?: string;
requestRegion: (ports: string[], availablePorts: string[], searchType: string, startDate: string, endDate: string) => void;
}

interface ErrorFieldMapping {
[key:string]: boolean
}

interface RegionalPressureDatesState {
start: Moment;
end: Moment;
}


const RegionalPressureDashboard = ({config, user, errors, type, start, end, requestRegion}: RegionalPressureDashboardProps) => {
const [searchType, setSearchType] = React.useState<string>(type || 'single');
const [dates, setDate] = React.useState<RegionalPressureDatesState>({
start: moment(start).subtract(1, 'day'),
end: moment(end),
});
const errorFieldMapping: ErrorFieldMapping = {}
errors.forEach((error: FormError) => errorFieldMapping[error.field] = true);

let userPortsByRegion: PortRegion[] = config.portsByRegion.map(region => {
const userPorts: string[] = user.ports.filter(p => region.ports.includes(p));
return {...region, ports: userPorts} as PortRegion
}).filter(r => r.ports.length > 0)

const availablePorts = config.ports.map(port => port.iata);

React.useEffect(() => {
requestRegion(user.ports, availablePorts, searchType, dates.start.format('YYYY-MM-DD'), dates.end.format('YYYY-MM-DD'));
}, [user, availablePorts, requestRegion, searchType, dates])

const handleSearchTypeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchType(event.target.value);
event.preventDefault();
};

const handleDateChange = (type: string, date: Moment | null) => {
setDate({
...dates,
[type]: date
});
requestRegion(user.ports, availablePorts, searchType, date!.format('YYYY-MM-DD'), dates.end.format('YYYY-MM-DD'));
}

return (
<Box>
<Box sx={{backgroundColor: '#E6E9F1', p: 2}}>
<Grid container spacing={2} justifyItems={'stretch'} sx={{mb:2}}>
<Grid item xs={12}>
<h1>National Dashboard</h1>
<h2>Compare pax arrivals vs previous year</h2>
</Grid>
<Grid item xs={12}>
<FormControl>
<FormLabel id="date-label">Select date</FormLabel>
<RadioGroup
row
aria-labelledby="date-label"
onChange={handleSearchTypeChange}
>
<FormControlLabel value="single" control={<Radio checked={searchType === 'single'} />} label="Single date" />
<FormControlLabel value="range" control={<Radio checked={searchType === 'range'} />} label="Date range" />
</RadioGroup>
</FormControl>
</Grid>
</Grid>


<Grid container spacing={2} justifyItems={'stretch'} sx={{mb:2}}>
<Grid item>
<DatePicker
slots={{
textField: TextField
}}
slotProps={{
textField: { error: !!errorFieldMapping.startDate }
}}
label="From"
sx={{backgroundColor: '#fff', marginRight: '10px'}}
value={dates.start}
onChange={(newValue: Moment | null) => handleDateChange('start', newValue)}/>

</Grid>
{ searchType === 'range' &&
<Grid item>
<DatePicker
slots={{
textField: TextField
}}
slotProps={{
textField: { error: !!errorFieldMapping.endDate }
}}
label="To"
sx={{backgroundColor: '#fff'}}
value={dates.end}
onChange={(newValue: Moment | null) => handleDateChange('end', newValue)}/>
</Grid>
}
</Grid>


<RegionalPressureViewSwitch config={config} userPortsByRegion={userPortsByRegion} />
</Box>
</Box>
)

}

const mapState = (state: RootState) => {
return {
errors: state.pressureDashboard?.errors,
type: state.pressureDashboard?.type,
startDate: state.pressureDashboard?.start,
endDate: state.pressureDashboard?.end,
};
}

const mapDispatch = (dispatch :MapDispatchToProps<any, RegionalPressureDashboardProps>) => {
return {
requestRegion: (userPorts: string[], availablePorts: string[], searchType: string, startDate: string, endDate: string) => {
dispatch(requestPaxTotals(userPorts, availablePorts, searchType, startDate, endDate));
}
};
};

export default connect(mapState, mapDispatch)(RegionalPressureDashboard);
Loading

0 comments on commit 44a5627

Please sign in to comment.