Данные моего компонента выбираются на основе введенного маршрута - /reports/:id
, т.е. /reports/1
"1" после /reports/
извлекается с помощью match.params.id
, который я затем отправляю по следующему адресу:
fetchDashData(`http://ee-etap.devops.fds.com/api/etap/v1/templates/template/report/${match.params.id}`)
Когда пользователь вводит недопустимый идентификатор, т.е. /reports/a
- я хочу перенаправить пользователя обратно на /reports
, который отображает целевую страницу и сообщение об ошибке, например:
return <Redirect to={{
pathname: '/reports',
state: { templateId: match.params.id } }}
/>;
Это все работает нормально до тех пор, пока пользователь не попытается посетить действительный «идентификатор», то есть /reports/1
сразу после ошибочного - /reports/a
, при котором пользователь немедленно перенаправляется обратно на страницу /reports
, потому что Вызов извлечения является асинхронным и не завершил загрузку данных для /reports/1
.
У меня уже определено состояние isLoading .. но как я могу предотвратить это?
ReportsDashboard.jsx (/reports/:id
)
class ChartsDashboard extends React.Component {
componentDidMount() {
const { fetchDashData, data, isLoading, hasErrored, match } = this.props;
if ( match.params && match.params.id ) {
fetchDashData(`http://ee-etap.devops.fds.com/api/etap/v1/templates/template/report/${match.params.id}`);
}
}
render() {
const { data, hasErrored, isLoading, classes, match } = this.props;
if ( isLoading ) {
return (
<div style={{ margin: '0 auto', textAlign: 'center' }}>
<CircularProgress size={50} color="secondary" />
</div>
);
}
if ( data && data !== null ) {
const { TemplateReport } = data;
const {
errorBarChart, historyTriggers, historyLineChart, jobs, lastBuildDonutChart, features,
} = TemplateReport;
if (errorBarChart.length === 0) {
// error in data
return <Redirect to={{
pathname: '/reports',
state: { templateId: match.params.id } }}
/>;
}
const keys = [];
errorBarChart.forEach((errorItem) => {
Object.keys(errorItem).forEach((errorKey) => {
if (errorKey !== 'category') {
keys.push(errorKey);
}
});
});
if (match.params.id) {
return (
<div className="page-container">
<Grid container spacing={24}>
<Grid item xs={12} lg={4}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Error By Categories</h4>
<div style={{ height: '350px' }}>
<ResponsiveBar
data={errorBarChart}
keys={keys}
indexBy="category"
margin={{
top: 50,
right: 50,
bottom: 50,
left: 50,
}}
padding={0.1}
colors="paired"
colorBy="id"
axisBottom={{
orient: 'bottom',
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'CATEGORY',
legendPosition: 'middle',
legendOffset: 36,
}}
axisLeft={{
orient: 'left',
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'ERROR COUNT',
legendPosition: 'middle',
legendOffset: -40,
}}
labelSkipWidth={12}
labelSkipHeight={12}
labelTextColor="inherit:darker(1.6)"
animate
motionStiffness={90}
motionDamping={15}
/>
</div>
</Paper>
</Grid>
<Grid item xs={12} lg={4}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Pass Rate %</h4>
<div style={{ height: '350px' }}>
<ResponsivePie
colors="paired"
colorBy={this.pieColors}
margin={{
top: 40,
right: 40,
bottom: 40,
left: 40,
}}
data={lastBuildDonutChart}
animate
defs={[
linearGradientDef('gradientRed', [{ offset: 0, color: 'red' }, { offset: 100, color: '#ffcdd2', opacity: 0.3 }]),
linearGradientDef('gradientYellow', [{ offset: 0, color: 'yellow' }, { offset: 100, color: '#f7bf18a3', opacity: 0.3 }]),
linearGradientDef('gradientGreen', [{ offset: 0, color: '#38da3e' }, { offset: 100, color: '#38da3e', opacity: 0.3 }]),
]}
fill={[
{ match: { id: 'Fail' }, id: 'gradientRed' },
{ match: { id: 'Pass' }, id: 'gradientGreen' },
{ match: { id: 'Undefined' }, id: 'gradientYellow' },
]}
radialLabelsSkipAngle={10}
radialLabelsTextXOffset={6}
radialLabelsTextColor="#333333"
radialLabelsLinkOffset={0}
radialLabelsLinkDiagonalLength={8}
radialLabelsLinkHorizontalLength={7}
radialLabelsLinkStrokeWidth={1}
radialLabelsLinkColor="inherit"
innerRadius={0.5}
padAngle={0.7}
cornerRadius={3}
/>
</div>
</Paper>
</Grid>
<Grid item xs={12} lg={4}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Jobs Triggered</h4>
<JobsTable data={jobs} templateId={match.params.id} />
</Paper>
</Grid>
<Grid item xs={12} lg={12}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Scenarios Table</h4>
<Tooltip title="Scenario Report">
<a href={`/reports/${match.params.id}/scenarioHistory`} rel="noopener noreferrer">
<IconButton aria-label="Scenario Report">
<AssignmentIcon />
</IconButton>
</a>
</Tooltip>
<ScenariosTable data={features} />
</Paper>
</Grid>
<Grid item xs={12} lg={12}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Execution History</h4>
<div style={{ height: '400px' }}>
<ResponsiveLine
colors="paired"
colorBy="id"
margin={{
top: 20,
right: 20,
bottom: 60,
left: 80,
}}
data={historyLineChart}
enableArea={true}
animate
yScale={{ type: 'linear', stacked: true }}
/>
</div>
</Paper>
</Grid>
<Grid item xs={12}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Previous Builds</h4>
<PreviousBuildsTable data={historyTriggers} templateId={match.params.id}/>
</Paper>
</Grid>
</Grid>
</div>
);
}
}
// error in data
return <Redirect to={{
pathname: '/reports',
state: { templateId: match.params.id } }}
/>;
}
}
const mapStateToProps = state => ({
data: state.reports.data,
hasErrored: state.reports.hasErrored,
isLoading: state.reports.isLoading,
});
const mapDispatchToProps = dispatch => ({
fetchDashData: url => dispatch(chartDataFetch(url)),
});
export default compose(
withStyles(styles),
withRouter,
connect(
mapStateToProps,
mapDispatchToProps,
),
)(ChartsDashboard);
BrowseReport.jsx (/reports/
)
class BrowseReports extends React.Component {
constructor(props) {
super(props);
this.state = {
searchVal: '',
errorMsg: '',
}
this.onSearchChange = this.onSearchChange.bind(this);
this.goToTemplateReport = this.goToTemplateReport.bind(this);
}
componentDidMount() {
if (this.props.location && this.props.location.state && this.props.location.state.templateId) {
this.state.errorMsg = `Template Name "${this.props.location.state.templateId}" does not exist, please try again`;
this.props.history.replace('/reports', null);
}
}
onSearchChange(val) {
this.setState({ searchVal: val });
}
goToTemplateReport(val) {
this.props.history.push(`/reports/${val}`);
}
render() {
const { classes, location } = this.props;
const { searchVal, errorMsg } = this.state;
return (
<div className="page-container" style={{ textAlign: 'center' }}>
<Grid container justify="center" spacing={24}>
<Grid item xs={12} lg={8}>
{/* dashData Error */}
<h4 className={classes.errorMsg}>
{/* ERROR MESSAGE HERE */}
{errorMsg}
</h4>
<SearchBar
value={this.state.searchVal}
placeholder='Search for Template Name'
onChange={(value) => this.onSearchChange(value)}
onRequestSearch={(value) => this.goToTemplateReport(value)}
style={{
margin: '0 auto',
}}
/>
</Grid>
<Grid item xs={12} lg={6}>
<Paper className={classes.paper}>
<CompletedJobsTable></CompletedJobsTable>
</Paper>
</Grid>
<Grid item xs={12} lg={6}>
<Paper className={classes.paper}>
<ActiveJobsTable></ActiveJobsTable>
</Paper>
</Grid>
</Grid>
</div>
)
}
}
export default compose(
withStyles(styles),
withRouter
)(BrowseReports);
actions.jsx
export const chartDataHasErrored = hasErrored => ({
type: CHARTS_DATA_HAS_ERRORED,
payload: { hasErrored },
});
export const chartDataIsLoading = isLoading => ({
type: CHARTS_DATA_IS_LOADING,
payload: { isLoading },
});
export const chartDataFetchSuccess = data => ({
type: CHARTS_DATA_FETCH_SUCCESS,
payload: { data },
});
export const chartDataFetch = url => (dispatch) => {
dispatch(chartDataIsLoading(true));
fetch(url, { mode: 'cors' })
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then(response => response.json())
.then((items) => {
dispatch(chartDataFetchSuccess(items));
})
.catch((error) => {
dispatch(chartDataHasErrored(error));
});
};
reducers.jsx
import { CHARTS_DATA_FETCH_SUCCESS, CHARTS_DATA_IS_LOADING, CHARTS_DATA_HAS_ERRORED } from '../../../store/actions';
const INITIAL_STATE = {
hasErrored: null,
isLoading: true,
data: {},
}
const reportsDashboardReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case CHARTS_DATA_HAS_ERRORED:
return {
...state,
hasErrored: action.payload.hasErrored,
isLoading: false,
};
case CHARTS_DATA_IS_LOADING:
return {
...state,
isLoading: action.payload.isLoading,
hasErrored: null,
};
case CHARTS_DATA_FETCH_SUCCESS:
return {
...state,
isLoading: false,
data: action.payload.data,
};
default:
return state;
}
};
export default reportsDashboardReducer;