diff --git a/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/__tests__/__snapshots__/index.spec.tsx.snap
index b54172120..47b356c2f 100644
--- a/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/__tests__/__snapshots__/index.spec.tsx.snap
+++ b/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/__tests__/__snapshots__/index.spec.tsx.snap
@@ -170,3 +170,60 @@ exports[`Issue component should render the certification error 1`] = `
`;
+
+exports[`Issue component should render the workspaceInactive error 1`] = `
+
+`;
diff --git a/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/__tests__/index.spec.tsx
index 2c087a314..51aeea44c 100644
--- a/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/__tests__/index.spec.tsx
+++ b/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/__tests__/index.spec.tsx
@@ -44,6 +44,17 @@ describe('Issue component', () => {
expect(renderer.create(component).toJSON()).toMatchSnapshot();
});
+ it('should render the workspaceInactive error', () => {
+ const issue = {
+ type: 'workspaceInactive',
+ error: new Error('The workspace is inactive.'),
+ data: { ideLoader: '', workspaceDetails: '' },
+ } as Issue;
+ const component = ;
+
+ expect(renderer.create(component).toJSON()).toMatchSnapshot();
+ });
+
it('should render an unknown error', () => {
const issue = {
type: 'unknown',
diff --git a/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/index.tsx b/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/index.tsx
index d697a030e..ab12251d2 100644
--- a/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/index.tsx
+++ b/packages/dashboard-frontend/src/Layout/ErrorReporter/Issue/index.tsx
@@ -14,7 +14,7 @@ import { TextContent, Text, TextVariants } from '@patternfly/react-core';
import { WarningTriangleIcon } from '@patternfly/react-icons';
import React from 'react';
import { BrandingData } from '../../../services/bootstrap/branding.constant';
-import { Issue } from '../../../services/bootstrap/issuesReporter';
+import { Issue, WorkspaceRoutes } from '../../../services/bootstrap/issuesReporter';
import * as styles from './index.module.css';
@@ -32,6 +32,8 @@ export class IssueComponent extends React.PureComponent {
return this.renderCertError();
case 'sso':
return this.renderSsoError(issue.error);
+ case 'workspaceInactive':
+ return this.renderWorkspaceInactiveError(issue.error, issue.data);
default:
return this.renderUnknownError(issue.error);
}
@@ -92,6 +94,53 @@ export class IssueComponent extends React.PureComponent {
);
}
+ private renderWorkspaceInactiveError(
+ error: Error,
+ workspaceRoutes: WorkspaceRoutes | undefined,
+ ): React.ReactNode {
+ const linkOnClick = (hash: string) => {
+ return () => {
+ window.location.hash = hash;
+ window.location.reload();
+ };
+ };
+
+ let ideLoader: React.ReactNode;
+ let workspaceDetails: React.ReactNode;
+
+ if (workspaceRoutes) {
+ ideLoader = (
+
+ Restart your workspace
+
+ );
+
+ workspaceDetails = (
+
+ Return to the dashboard
+
+ );
+ }
+
+ const warningTextbox = !error ? undefined : (
+
+ {error.message}
+
+ );
+
+ return (
+
+
+
+ Warning
+
+ {warningTextbox}
+ {ideLoader}
+ {workspaceDetails}
+
+ );
+ }
+
private renderUnknownError(error: Error): React.ReactNode {
const errorTextbox = !error ? undefined : (
diff --git a/packages/dashboard-frontend/src/preload/__tests__/location.spec.ts b/packages/dashboard-frontend/src/preload/__tests__/location.spec.ts
index a700c8117..00905afd5 100644
--- a/packages/dashboard-frontend/src/preload/__tests__/location.spec.ts
+++ b/packages/dashboard-frontend/src/preload/__tests__/location.spec.ts
@@ -10,7 +10,8 @@
* Red Hat, Inc. - initial API and implementation
*/
-import { buildFactoryLoaderPath } from '../';
+import SessionStorageService, { SessionStorageKey } from '../../services/session-storage';
+import { buildFactoryLoaderPath, storePathIfNeeded } from '../';
describe('Location test', () => {
test('new policy', () => {
@@ -49,3 +50,22 @@ describe('Location test', () => {
);
});
});
+
+describe('storePathnameIfNeeded test', () => {
+ let mockUpdate: jest.Mock;
+
+ beforeAll(() => {
+ mockUpdate = jest.fn();
+ SessionStorageService.update = mockUpdate;
+ });
+
+ test('empty path', () => {
+ storePathIfNeeded('/');
+ expect(mockUpdate).toBeCalledTimes(0);
+ });
+
+ test('regular path', () => {
+ storePathIfNeeded('/test');
+ expect(mockUpdate).toBeCalledWith(SessionStorageKey.ORIGINAL_LOCATION_PATH, '/test');
+ });
+});
diff --git a/packages/dashboard-frontend/src/preload/index.ts b/packages/dashboard-frontend/src/preload/index.ts
index 72b13c935..23d01dc89 100644
--- a/packages/dashboard-frontend/src/preload/index.ts
+++ b/packages/dashboard-frontend/src/preload/index.ts
@@ -10,10 +10,15 @@
* Red Hat, Inc. - initial API and implementation
*/
+import SessionStorageService, { SessionStorageKey } from '../services/session-storage';
+
(function acceptNewFactoryLink(): void {
if (window.location.pathname.startsWith('/dashboard/')) {
return;
}
+
+ storePathIfNeeded(window.location.pathname);
+
const hash = window.location.hash.replace(/(\/?)#(\/?)/, '#');
if (hash.startsWith('#http')) {
let factoryUrl = hash.substring(1);
@@ -27,6 +32,12 @@
}
})();
+export function storePathIfNeeded(path: string) {
+ if (path !== '/') {
+ SessionStorageService.update(SessionStorageKey.ORIGINAL_LOCATION_PATH, path);
+ }
+}
+
export function buildFactoryLoaderPath(url: string): string {
const fullUrl = new window.URL(url);
const editor = extractUrlParam(fullUrl, 'che-editor');
diff --git a/packages/dashboard-frontend/src/services/bootstrap/index.ts b/packages/dashboard-frontend/src/services/bootstrap/index.ts
index d51a6fc2a..854d04042 100644
--- a/packages/dashboard-frontend/src/services/bootstrap/index.ts
+++ b/packages/dashboard-frontend/src/services/bootstrap/index.ts
@@ -38,6 +38,9 @@ import { selectDefaultNamespace } from '../../store/InfrastructureNamespaces/sel
import { selectDevWorkspacesResourceVersion } from '../../store/Workspaces/devWorkspaces/selectors';
import { AppAlerts } from '../alerts/appAlerts';
import { AlertVariant } from '@patternfly/react-core';
+import SessionStorageService, { SessionStorageKey } from '../session-storage';
+import { buildDetailsLocation, buildIdeLoaderLocation } from '../helpers/location';
+import { selectAllWorkspaces } from '../../store/Workspaces/selectors';
/**
* This class executes a few initial instructions
@@ -80,10 +83,11 @@ export default class Bootstrap {
this.fetchRegistriesMetadata(settings),
this.watchNamespaces(),
this.updateDevWorkspaceTemplates(settings),
- this.fetchWorkspaces(),
+ this.fetchWorkspaces().then(() => this.checkInactivityShutdown()),
this.fetchClusterInfo(),
this.fetchClusterConfig(),
]);
+
const errors = results
.filter(result => result.status === 'rejected')
.map(result => (result as PromiseRejectedResult).reason.toString());
@@ -267,4 +271,34 @@ export default class Bootstrap {
const { requestUserProfile } = UserProfileStore.actionCreators;
return requestUserProfile()(this.store.dispatch, this.store.getState, undefined);
}
+
+ private checkInactivityShutdown() {
+ const path = SessionStorageService.remove(SessionStorageKey.ORIGINAL_LOCATION_PATH);
+ if (!path) {
+ return;
+ }
+
+ const state = this.store.getState();
+
+ const workspace = selectAllWorkspaces(state).find(w => w.ideUrl?.includes(path));
+ if (!workspace) {
+ return;
+ }
+
+ if (workspace.isRunning && workspace.ideUrl) {
+ window.location.href = workspace.ideUrl;
+ return;
+ }
+
+ const ideLoader = buildIdeLoaderLocation(workspace).pathname;
+ const workspaceDetails = buildDetailsLocation(workspace).pathname;
+
+ this.issuesReporterService.registerIssue(
+ 'workspaceInactive',
+ new Error(
+ 'Your workspace has stopped. This could happen if your workspace shuts down due to inactivity.',
+ ),
+ { ideLoader, workspaceDetails },
+ );
+ }
}
diff --git a/packages/dashboard-frontend/src/services/bootstrap/issuesReporter.ts b/packages/dashboard-frontend/src/services/bootstrap/issuesReporter.ts
index e3be05175..f7042682b 100644
--- a/packages/dashboard-frontend/src/services/bootstrap/issuesReporter.ts
+++ b/packages/dashboard-frontend/src/services/bootstrap/issuesReporter.ts
@@ -12,12 +12,15 @@
import { injectable } from 'inversify';
-export type IssueType = 'cert' | 'sso' | 'unknown';
+export type IssueType = 'cert' | 'sso' | 'workspaceInactive' | 'unknown';
export type Issue = {
type: IssueType;
error: Error;
+ data?: WorkspaceRoutes;
};
+export type WorkspaceRoutes = { ideLoader: string; workspaceDetails: string };
+
@injectable()
export class IssuesReporterService {
private issues: Issue[] = [];
@@ -26,8 +29,8 @@ export class IssuesReporterService {
return this.issues.length !== 0;
}
- public registerIssue(type: IssueType, error: Error): void {
- this.issues.push({ type, error });
+ public registerIssue(type: IssueType, error: Error, data?: WorkspaceRoutes): void {
+ this.issues.push({ type, error, data });
}
public reportIssue(): Issue | undefined {
diff --git a/packages/dashboard-frontend/src/services/session-storage/index.ts b/packages/dashboard-frontend/src/services/session-storage/index.ts
index 2c042ae13..96f109d62 100644
--- a/packages/dashboard-frontend/src/services/session-storage/index.ts
+++ b/packages/dashboard-frontend/src/services/session-storage/index.ts
@@ -12,6 +12,7 @@
export enum SessionStorageKey {
PRIVATE_FACTORY_RELOADS = 'private-factory-reloads-number',
+ ORIGINAL_LOCATION_PATH = 'original-location-path',
}
export default class SessionStorageService {
@@ -22,4 +23,10 @@ export default class SessionStorageService {
static get(key: SessionStorageKey): string | undefined {
return window.sessionStorage.getItem(key) || undefined;
}
+
+ static remove(key: SessionStorageKey): string | undefined {
+ const value = this.get(key);
+ window.sessionStorage.removeItem(key);
+ return value;
+ }
}