Skip to content

Commit

Permalink
Handle and display a specialized error view for startup errors
Browse files Browse the repository at this point in the history
Fixes #55
  • Loading branch information
jumpinjackie committed Oct 4, 2016
1 parent de9a9ea commit 6259685
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 26 deletions.
56 changes: 46 additions & 10 deletions src/actions/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ import {
} from "../api/common";
import { IView } from "../api/contracts/common";
import { RuntimeMap } from "../api/contracts/runtime-map";
import { tr } from "../api/i18n";
import { MgError } from "../api/error";
import * as logger from "../utils/logger";
import queryString = require("query-string");
const parse = require("url-parse");
const assign = require("object-assign");
const proj4 = require("proj4");

function convertUIItems(items: UIItem[] | null | undefined, cmdsByKey: any, noToolbarLabels = true, canSupportFlyouts = true): any[] {
return (items || []).map(item => {
Expand Down Expand Up @@ -173,20 +176,53 @@ export function initLayout(options: any): ReduxThunkedAction {
};
const onSessionAcquired = (session: string) => {
client.getResource<WebLayout>(opts.resourceId, { SESSION: session }).then(wl => {
return Promise.all([
wl,
client.createRuntimeMap({
mapDefinition: wl.Map.ResourceId,
requestedFeatures: RuntimeMapFeatureFlags.LayerFeatureSources | RuntimeMapFeatureFlags.LayerIcons | RuntimeMapFeatureFlags.LayersAndGroups,
session: session
})
]);
}).then(onWebLayoutAndRuntimeMapReceived);
return client.createRuntimeMap({
mapDefinition: wl.Map.ResourceId,
requestedFeatures: RuntimeMapFeatureFlags.LayerFeatureSources | RuntimeMapFeatureFlags.LayerIcons | RuntimeMapFeatureFlags.LayersAndGroups,
session: session
}).then(map => {
//Check the EPSG code here
const epsg = map.CoordinateSystem.EpsgCode;

//Must be non-zero
if (epsg == "0") {
throw new MgError(tr("INIT_ERROR_UNSUPPORTED_COORD_SYS", opts.locale || "en", { mapDefinition: wl.Map.ResourceId }));
}
//Must be registered to proj4js if not 4326 or 3857
//TODO: We should allow for online fallback (eg. Hit epsg.io for the proj4js definition)
if (!proj4.defs[`EPSG:${epsg}`]) {
throw new MgError(tr("INIT_ERROR_UNREGISTERED_EPSG_CODE", opts.locale || "en", { epsg: epsg, mapDefinition: wl.Map.ResourceId }));
}

return Promise.all([
wl,
map
]);
});
}).then(onWebLayoutAndRuntimeMapReceived).catch(err => {
dispatch({
type: Constants.INIT_ERROR,
payload: {
error: err,
options: opts
}
});
})
}
if (opts.session) {
onSessionAcquired(opts.session);
} else {
client.createSession("Anonymous", "").then(onSessionAcquired);
client.createSession("Anonymous", "")
.then(onSessionAcquired)
.catch(err => {
dispatch({
type: Constants.INIT_ERROR,
payload: {
error: err,
options: opts
}
});
});
}
}
};
Expand Down
6 changes: 6 additions & 0 deletions src/api/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,13 @@ export interface IConfigurationReducerState {
capabilities: IViewerCapabilities;
}

export interface IInitErrorReducerState {
error: Error | undefined;
options: any;
}

export interface IApplicationState {
initError: IInitErrorReducerState;
config: IConfigurationReducerState;
map: IMapReducerState;
legend: ILegendReducerState;
Expand Down
19 changes: 12 additions & 7 deletions src/components/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ function isError(err: Error|string): err is Error {

export interface IErrorProps {
error: Error|string;
errorRenderer?: (err: Error) => JSX.Element;
}

export const Error = (props: IErrorProps) => {
const err = props.error;
if (isError(err)) {
const stack = err.stack || "";
return <div className="error-display">
<div className="error-header">{err.message}</div>
<ul className="error-stack">
{stack.split("\n").map((ln, i) => <li key={`stack-line-${i}`}>{ln}</li>)}
</ul>
</div>;
if (props.errorRenderer) {
return props.errorRenderer(err);
} else {
const stack = err.stack || "";
return <div className="error-display">
<div className="error-header">{err.message}</div>
<ul className="error-stack">
{stack.split("\n").map((ln, i) => <li key={`stack-line-${i}`}>{ln}</li>)}
</ul>
</div>;
}
} else {
return <div className="error-display">
<div className="error-header">{err}</div>
Expand Down
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const INIT_APP = 'MapGuide/INIT_APP';
export const INIT_ERROR = 'MapGuide/INIT_ERROR';

export const LEGEND_SET_GROUP_VISIBILITY = 'Legend/SET_GROUP_VISIBILITY';
export const LEGEND_SET_LAYER_VISIBILITY = 'Legend/SET_LAYER_VISIBILITY';
Expand Down
70 changes: 62 additions & 8 deletions src/containers/app.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import * as React from "react";
import { connect } from "react-redux";
import { getLayout } from "../api/registry/layout";
import { IExternalBaseLayer, ReduxDispatch, IApplicationState, ClientKind } from "../api/common";
import {
IExternalBaseLayer,
ReduxDispatch,
IApplicationState,
IConfigurationReducerState,
IInitErrorReducerState,
ClientKind
} from "../api/common";
import { initLayout } from "../actions/init";
import { Error } from "../components/error";
import { tr } from "../api/i18n";
Expand Down Expand Up @@ -35,7 +42,9 @@ export interface IAppProps {
}

export interface IAppState {
config?: any;
error?: Error;
initOptions?: any;
config?: IConfigurationReducerState;
}

export interface IInitAppLayout {
Expand All @@ -50,6 +59,8 @@ export interface IAppDispatch {

function mapStateToProps(state: IApplicationState): IAppState {
return {
error: state.initError.error,
initOptions: state.initError.options,
config: state.config
};
}
Expand All @@ -64,8 +75,10 @@ export type AppProps = IAppProps & IAppState & IAppDispatch;

@connect(mapStateToProps, mapDispatchToProps)
export class App extends React.Component<AppProps, any> {
private fnErrorRenderer: (err: Error) => JSX.Element;
constructor(props: AppProps) {
super(props);
this.fnErrorRenderer = this.initErrorRenderer.bind(this);
}
componentDidMount() {
const { initApp, initLayout, agent, resourceId, externalBaseLayers } = this.props;
Expand All @@ -76,15 +89,56 @@ export class App extends React.Component<AppProps, any> {
});
}
}
private renderErrorMessage(msg: string, locale: string, args: any): JSX.Element {
switch (msg) {
case "MgConnectionFailedException":
{
const arg = { __html: tr("INIT_ERROR_NO_CONNECTION", locale) };
return <div dangerouslySetInnerHTML={arg} />;
}
case "MgResourceNotFoundException":
{
const arg = { __html: tr("INIT_ERROR_RESOURCE_NOT_FOUND", locale, { resourceId: args.resourceId }) };
return <div dangerouslySetInnerHTML={arg} />;
}
case "MgSessionExpiredException":
{
const arg = { __html: tr("INIT_ERROR_EXPIRED_SESSION", locale, { sessionId: args.session }) };
return <div dangerouslySetInnerHTML={arg} />;
}
default:
{
const arg = { __html: msg };
return <div dangerouslySetInnerHTML={arg} />;
}
}
}
private initErrorRenderer(err: Error): JSX.Element {
const { config, initOptions } = this.props;
let locale = config ? (config.locale || "en") : "en";
if (initOptions && initOptions.locale) {
locale = initOptions.locale;
}
//Not showing stack as the error cases are well-defined here and we know where they
//originate from
return <div>
<h1>{tr("INIT_ERROR_TITLE", locale)}</h1>
{this.renderErrorMessage(err.message, locale, initOptions || {})}
</div>;
}
render(): JSX.Element {
const { layout, config } = this.props;
const { layout, config, error } = this.props;
const layoutEl = getLayout(layout);
//NOTE: Locale may not have been set at this point, so use default
const locale = config.locale || "en";
if (layoutEl) {
return layoutEl();
if (error) {
return <Error error={error} errorRenderer={this.fnErrorRenderer} />
} else {
return <Error error={tr("ERR_UNREGISTERED_LAYOUT", locale, { layout: layout })} />;
//NOTE: Locale may not have been set at this point, so use default
const locale = config ? (config.locale || "en") : "en";
if (layoutEl) {
return layoutEl();
} else {
return <Error error={tr("ERR_UNREGISTERED_LAYOUT", locale, { layout: layout })} />;
}
}
}
}
2 changes: 2 additions & 0 deletions src/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { legendReducer } from "./legend";
import { taskPaneReducer } from "./taskpane";
import { lastAction } from "./last-action";
import { modalReducer } from "./modal";
import { initErrorReducer } from "./initError";

const rootReducer = combineReducers({
initError: initErrorReducer,
config: configReducer,
map: runtimeMapReducer,
session: sessionReducer,
Expand Down
25 changes: 25 additions & 0 deletions src/reducers/initError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as Constants from "../constants";
import { IInitErrorReducerState } from "../api/common";
const assign = require("object-assign");

const INITIAL_STATE: IInitErrorReducerState = {
options: {},
error: undefined
};

export function initErrorReducer(state = INITIAL_STATE, action = { type: '', payload: null }) {
switch (action.type) {
case Constants.INIT_ERROR:
{
const payload: any | null = action.payload;
if (payload) {
const error = payload.error;
const options = payload.options;
if (error instanceof Error) {
return { error: error, options: options };
}
}
}
}
return state;
}
6 changes: 6 additions & 0 deletions src/strings/en.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
const STRINGS_EN: any = {
"INIT_ERROR_TITLE": "An error occurred during startup",
"INIT_ERROR_UNSUPPORTED_COORD_SYS": "<p>The Map Definition <strong>{mapDefinition}</strong>, uses a coordinate system that does not resolve to a valid EPSG code and cannot be loaded in this viewer</p><p>Solution:</p><ul><li>Change the coordinate system of this Map Definition to one that resolves to an EPSG code</li><li>Please note: There will be a small performance overhead for server-side re-projection as a result of doing this</li></ul>",
"INIT_ERROR_UNREGISTERED_EPSG_CODE": "<p>The Map Definition <strong>{mapDefinition}</strong>, uses a coordinate system that resolves to a valid EPSG code (<strong>EPSG:{epsg}</strong>), but no projection for this EPSG code has been registered</p><p>Solution:</p><ol><li>Search for the matching proj4js definition at <a href='http://epsg.io/'>http://epsg.io/</a></li><li>Register this projection to the viewer before mounting it</li></ol>",
"INIT_ERROR_EXPIRED_SESSION": "<p>The session id given has expired: <strong>{sessionId}</strong></p><p>Reload the viewer without the <strong>session</strong> parameter, or supply a valid session id for the <strong>session</strong> parameter</p>",
"INIT_ERROR_RESOURCE_NOT_FOUND": "Attempted to load the following resource, but it was not found: <strong>{resourceId}</strong>",
"INIT_ERROR_NO_CONNECTION": "<p>There is no connection between the MapGuide Web Tier and the MapGuide Server.</p><p>Possible causes:</p><ul><li>MapGuide Server is not running or is no longer responding</li><li>Internet connection problems</li></ul><p>Possible solutions:</p><ul><li>Restart the MapGuide Server Service</li><li>Contact your server administrator</li></ul>",
"TPL_SIDEBAR_OPEN_TASKPANE": "Open Task Pane",
"TPL_SIDEBAR_OPEN_LEGEND": "Open Legend",
"TPL_SIDEBAR_OPEN_SELECTION_PANEL": "Open Selection Panel",
Expand Down
2 changes: 1 addition & 1 deletion viewer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#map { position: absolute; left: 0; right: 0; top: 0; bottom: 0; }
</style>
<script type="text/javascript" src="dist/viewer.js"></script>
<script type="text/javascript" src="strings/de.js"></script>
<script type="text/javascript" src="strings/de.js" charset="UTF-8"></script>
</head>
<body>
<div id="map"></div>
Expand Down
7 changes: 7 additions & 0 deletions viewer/strings/de.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
//NOTE: This is a quick-and-dirty Google Translation (for verifying the localization system is working)
var STRINGS_DE = {
"INIT_ERROR_TITLE": "Ein Fehler ist aufgetreten während des Starts",
"INIT_ERROR_UNSUPPORTED_COORD_SYS": "<p>Die Karte Definition <strong>{mapDefinition}</strong>, verwendet ein Koordinatensystem, das auf einen gültigen EPSG-Code löst nicht und nicht in diesem Viewer geladen werden kann</p><p>Lösung:</p><ul><li>ändern Sie das System dieser Karte Definition auf eine Koordinate, die</li><li>zu einem EPSG Code löst Bitte beachten Sie: Es wird eine kleine Performance-Overhead für die serverseitige Rückprojektion sein als Ergebnis dies zu tun</li></ul>",
"INIT_ERROR_UNREGISTERED_EPSG_CODE": "<p>Die Karte Definition <strong>{mapDefinition}</strong>, verwendet ein Koordinatensystem, das auf einen gültigen EPSG-Code löst (<strong>EPSG:{epsg}</strong>), aber keine Projektion für dieses EPSG-Code registriert</p><p>Solution wurde:</p><ol><li>Suche nach dem passenden proj4js Definition unter <a href='http://epsg.io/'>http://epsg.io/</a></li><li>diese Projektion für den Betrachter Registrieren sie vor der Montage</li></ol>",
"INIT_ERROR_EXPIRED_SESSION": "<p>Die Session-ID angegeben ist abgelaufen: <strong>{sessionId}</strong></p><p>Neu laden den Betrachter ohne die <strong>session</strong> parameter oder liefern eine gültige Session-ID der <strong>session</strong> parameter</p>",
"INIT_ERROR_RESOURCE_NOT_FOUND": "Versuchte die folgende Ressource zu laden, aber es wurde nicht gefunden: <strong>{resourceId}</strong>",
"INIT_ERROR_NO_CONNECTION": "<p>Es gibt keine Verbindung zwischen dem MapGuide Web Tier und dem MapGuide Server</p><p>Mögliche Ursachen:</p><ul><li>MapGuide Server läuft nicht oder nicht mehr reagiert</li><li>Internet-Verbindungsprobleme</li></ul><p>Mögliche Lösungen:</p><ul><li>die MapGuide Server-Dienst neu starten</li><li>Wenden Sie sich Server-Administrator</li></ul>",
"TPL_SIDEBAR_OPEN_TASKPANE": "Öffnen Sie den Task Pane",
"TPL_SIDEBAR_OPEN_LEGEND": "Öffnen Legend",
"TPL_SIDEBAR_OPEN_SELECTION_PANEL": "Öffnen Auswahljury",
Expand Down

0 comments on commit 6259685

Please sign in to comment.