diff --git a/public/app/core/components/PageLoader/PageLoader.tsx b/public/app/core/components/PageLoader/PageLoader.tsx index 6deeabf9a41..ff4f4dc4c20 100644 --- a/public/app/core/components/PageLoader/PageLoader.tsx +++ b/public/app/core/components/PageLoader/PageLoader.tsx @@ -1,4 +1,5 @@ import React, { FC } from 'react'; +import { LoadingPlaceholder } from '@grafana/ui'; interface Props { pageName?: string; @@ -8,8 +9,7 @@ const PageLoader: FC = ({ pageName = '' }) => { const loadingText = `Loading ${pageName}...`; return (
- -
{loadingText}
+
); }; diff --git a/public/app/core/components/SafeDynamicImport.tsx b/public/app/core/components/SafeDynamicImport.tsx new file mode 100644 index 00000000000..11eba3b7ef7 --- /dev/null +++ b/public/app/core/components/SafeDynamicImport.tsx @@ -0,0 +1,52 @@ +import React, { lazy, Suspense, FunctionComponent } from 'react'; +import { cx, css } from 'emotion'; +import { LoadingPlaceholder, ErrorBoundary, Button } from '@grafana/ui'; + +export const LoadingChunkPlaceHolder: FunctionComponent = () => ( +
+ +
+); + +function getAlertPageStyle() { + return css` + width: 508px; + margin: 128px auto; + `; +} + +export const SafeDynamicImport = (importStatement: Promise) => ({ ...props }) => { + const LazyComponent = lazy(() => importStatement); + return ( + + {({ error, errorInfo }) => { + if (!errorInfo) { + return ( + }> + + + ); + } + + return ( +
+

Unable to find application file

+
+

Grafana has likely been updated. Please try reloading the page.

+
+
+ +
+
+ {error && error.toString()} +
+ {errorInfo.componentStack} +
+
+ ); + }} +
+ ); +}; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 1c7f7821f2e..b21243984bc 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -1,7 +1,6 @@ import './dashboard_loaders'; import './ReactContainer'; import { applyRouteRegistrationHandlers } from './registry'; - // Pages import CreateFolderCtrl from 'app/features/folders/CreateFolderCtrl'; import FolderDashboardsCtrl from 'app/features/folders/FolderDashboardsCtrl'; @@ -10,10 +9,10 @@ import LdapPage from 'app/features/admin/ldap/LdapPage'; import LdapUserPage from 'app/features/admin/ldap/LdapUserPage'; import config from 'app/core/config'; import { route, ILocationProvider } from 'angular'; - // Types import { DashboardRouteInfo } from 'app/types'; import { LoginPage } from 'app/core/components/Login/LoginPage'; +import { SafeDynamicImport } from '../core/components/SafeDynamicImport'; /** @ngInject */ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locationProvider: ILocationProvider) { @@ -23,7 +22,7 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati // ones. That means angular ones could be navigated to in case there is a client side link some where. const importDashboardPage = () => - import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPage'); + SafeDynamicImport(import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPage')); $routeProvider .when('/', { @@ -79,7 +78,9 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati reloadOnSearch: false, resolve: { component: () => - import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage'), + SafeDynamicImport( + import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') + ), }, }) .when('/dashboard-solo/:type/:slug', { @@ -89,7 +90,9 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati reloadOnSearch: false, resolve: { component: () => - import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage'), + SafeDynamicImport( + import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') + ), }, }) .when('/dashboard/import', { @@ -101,7 +104,9 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati template: '', resolve: { component: () => - import(/* webpackChunkName: "DataSourcesListPage"*/ 'app/features/datasources/DataSourcesListPage'), + SafeDynamicImport( + import(/* webpackChunkName: "DataSourcesListPage"*/ 'app/features/datasources/DataSourcesListPage') + ), }, }) .when('/datasources/edit/:id/', { @@ -109,20 +114,27 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati reloadOnSearch: false, // for tabs resolve: { component: () => - import(/* webpackChunkName: "DataSourceSettingsPage"*/ '../features/datasources/settings/DataSourceSettingsPage'), + SafeDynamicImport( + import(/* webpackChunkName: "DataSourceSettingsPage"*/ '../features/datasources/settings/DataSourceSettingsPage') + ), }, }) .when('/datasources/edit/:id/dashboards', { template: '', resolve: { component: () => - import(/* webpackChunkName: "DataSourceDashboards"*/ 'app/features/datasources/DataSourceDashboards'), + SafeDynamicImport( + import(/* webpackChunkName: "DataSourceDashboards"*/ 'app/features/datasources/DataSourceDashboards') + ), }, }) .when('/datasources/new', { template: '', resolve: { - component: () => import(/* webpackChunkName: "NewDataSourcePage"*/ '../features/datasources/NewDataSourcePage'), + component: () => + SafeDynamicImport( + import(/* webpackChunkName: "NewDataSourcePage"*/ '../features/datasources/NewDataSourcePage') + ), }, }) .when('/dashboards', { @@ -138,13 +150,19 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati .when('/dashboards/f/:uid/:slug/permissions', { template: '', resolve: { - component: () => import(/* webpackChunkName: "FolderPermissions"*/ 'app/features/folders/FolderPermissions'), + component: () => + SafeDynamicImport( + import(/* webpackChunkName: "FolderPermissions"*/ 'app/features/folders/FolderPermissions') + ), }, }) .when('/dashboards/f/:uid/:slug/settings', { template: '', resolve: { - component: () => import(/* webpackChunkName: "FolderSettingsPage"*/ 'app/features/folders/FolderSettingsPage'), + component: () => + SafeDynamicImport( + import(/* webpackChunkName: "FolderSettingsPage"*/ 'app/features/folders/FolderSettingsPage') + ), }, }) .when('/dashboards/f/:uid/:slug', { @@ -162,7 +180,7 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati reloadOnSearch: false, resolve: { roles: () => (config.viewersCanEdit ? [] : ['Editor', 'Admin']), - component: () => import(/* webpackChunkName: "explore" */ 'app/features/explore/Wrapper'), + component: () => SafeDynamicImport(import(/* webpackChunkName: "explore" */ 'app/features/explore/Wrapper')), }, }) .when('/a/:pluginId/', { @@ -170,13 +188,15 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati template: '', reloadOnSearch: false, resolve: { - component: () => import(/* webpackChunkName: "AppRootPage" */ 'app/features/plugins/AppRootPage'), + component: () => + SafeDynamicImport(import(/* webpackChunkName: "AppRootPage" */ 'app/features/plugins/AppRootPage')), }, }) .when('/org', { template: '', resolve: { - component: () => import(/* webpackChunkName: "OrgDetailsPage" */ '../features/org/OrgDetailsPage'), + component: () => + SafeDynamicImport(import(/* webpackChunkName: "OrgDetailsPage" */ '../features/org/OrgDetailsPage')), }, }) .when('/org/new', { @@ -186,7 +206,8 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati .when('/org/users', { template: '', resolve: { - component: () => import(/* webpackChunkName: "UsersListPage" */ 'app/features/users/UsersListPage'), + component: () => + SafeDynamicImport(import(/* webpackChunkName: "UsersListPage" */ 'app/features/users/UsersListPage')), }, }) .when('/org/users/invite', { @@ -198,14 +219,15 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati template: '', resolve: { roles: () => ['Editor', 'Admin'], - component: () => import(/* webpackChunkName: "ApiKeysPage" */ 'app/features/api-keys/ApiKeysPage'), + component: () => + SafeDynamicImport(import(/* webpackChunkName: "ApiKeysPage" */ 'app/features/api-keys/ApiKeysPage')), }, }) .when('/org/teams', { template: '', resolve: { roles: () => (config.editorsCanAdmin ? [] : ['Editor', 'Admin']), - component: () => import(/* webpackChunkName: "TeamList" */ 'app/features/teams/TeamList'), + component: () => SafeDynamicImport(import(/* webpackChunkName: "TeamList" */ 'app/features/teams/TeamList')), }, }) .when('/org/teams/new', { @@ -217,7 +239,7 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati template: '', resolve: { roles: () => (config.editorsCanAdmin ? [] : ['Admin']), - component: () => import(/* webpackChunkName: "TeamPages" */ 'app/features/teams/TeamPages'), + component: () => SafeDynamicImport(import(/* webpackChunkName: "TeamPages" */ 'app/features/teams/TeamPages')), }, }) .when('/profile', { @@ -228,7 +250,10 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati .when('/profile/password', { template: '', resolve: { - component: () => import(/* webPackChunkName: "ChangePasswordPage" */ 'app/features/profile/ChangePasswordPage'), + component: () => + SafeDynamicImport( + import(/* webPackChunkName: "ChangePasswordPage" */ 'app/features/profile/ChangePasswordPage') + ), }, }) .when('/profile/select-org', { @@ -278,7 +303,8 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati .when('/admin/stats', { template: '', resolve: { - component: () => import(/* webpackChunkName: "ServerStats" */ 'app/features/admin/ServerStats'), + component: () => + SafeDynamicImport(import(/* webpackChunkName: "ServerStats" */ 'app/features/admin/ServerStats')), }, }) .when('/admin/ldap', { @@ -323,14 +349,16 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati .when('/plugins', { template: '', resolve: { - component: () => import(/* webpackChunkName: "PluginListPage" */ 'app/features/plugins/PluginListPage'), + component: () => + SafeDynamicImport(import(/* webpackChunkName: "PluginListPage" */ 'app/features/plugins/PluginListPage')), }, }) .when('/plugins/:pluginId/', { template: '', reloadOnSearch: false, // tabs from query parameters resolve: { - component: () => import(/* webpackChunkName: "PluginPage" */ '../features/plugins/PluginPage'), + component: () => + SafeDynamicImport(import(/* webpackChunkName: "PluginPage" */ '../features/plugins/PluginPage')), }, }) .when('/plugins/:pluginId/page/:slug', { @@ -350,7 +378,8 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati template: '', reloadOnSearch: false, resolve: { - component: () => import(/* webpackChunkName: "AlertRuleList" */ 'app/features/alerting/AlertRuleList'), + component: () => + SafeDynamicImport(import(/* webpackChunkName: "AlertRuleList" */ 'app/features/alerting/AlertRuleList')), }, }) .when('/alerting/notifications', {