Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect if dashboard has been accessed due to workspace idle redirect + workspace idle error #556

Merged
merged 1 commit into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,60 @@ exports[`Issue component should render the certification error 1`] = `
</p>
</div>
`;

exports[`Issue component should render the workspaceInactive error 1`] = `
<div
className="pf-c-content"
>
<h1
className=""
data-pf-content={true}
>
<svg
aria-hidden={true}
aria-labelledby={null}
fill="currentColor"
height="1em"
role="img"
style={
Object {
"verticalAlign": "-0.125em",
}
}
viewBox="0 0 1088 1024"
width="1em"
>
<path
d="M1057.10141,663.5 L845.101405,215.4 C787.101405,71.8 665.401405,0 542.901405,0 C420.201405,0 296.701405,71.9 235.001405,215.6 L31.7014051,648.5 C10.4014051,700 -0.0985948775,752.3 0.000697596367,800.8 C0.301405123,924.8 70.2014051,1024 209.101405,1024 L868.401405,1024 C1005.80141,1024 1087.70141,918.6 1088.00215,795.5 C1088.10141,752.4 1078.20141,707.2 1057.10141,663.5 Z M959.401405,800.3 C958.701405,822.9 952.901405,843.5 942.601405,859.7 C926.801405,884.6 902.601405,896.7 868.301405,896.7 L209.101405,896.7 C191.201405,896.7 176.601405,893.8 165.401405,888.2 C157.301405,884 150.801405,878.4 145.401405,870.3 C135.101405,855 129.101405,832 128.401405,805.6 C127.601405,772.8 134.901405,736.5 149.401405,700.5 L353.001405,266.7 C363.201405,242.9 376.101405,221.5 391.101405,203.2 C404.801405,186.6 420.301405,172.4 437.401405,161.1 C469.201405,139.9 505.701405,128.8 542.901405,128.8 C579.701405,128.8 615.401405,139.8 646.001405,160.5 C662.401405,171.6 677.101405,185.4 690.101405,201.6 C704.501405,219.6 716.401405,240.6 725.901405,264 L940.901405,718.9 L941.101405,719.3 L941.301405,719.7 C953.901405,746 960.201405,773.9 959.401405,800.3 Z M586.601405,832 L501.301405,832 C489.501405,831.8 480.201405,821.5 480.001405,808.7 L480.001405,727.3 C480.201405,714.5 489.601405,704.3 501.301405,704 L586.601405,704 C598.401405,704.2 607.701405,714.5 607.901405,727.3 L607.901405,808.7 L608.001405,808.7 C607.701405,821.5 598.301405,831.8 586.601405,832 M639.901405,290.7 L613.201405,610.4 C611.801405,626.9 598.001405,640 581.301405,640 L506.601405,640 C490.001405,640 476.101405,627.2 474.701405,610.7 L448.101405,291 C446.501405,272.3 461.301405,256.3 480.001405,256.3 L608.001405,256 C626.701405,256 641.401405,272 639.901405,290.7"
/>
</svg>
Warning
</h1>
<pre
className=""
data-pf-content={true}
>
The workspace is inactive.
</pre>
<p
className=""
data-pf-content={true}
>
<a
onClick={[Function]}
>
Restart your workspace
</a>
</p>
<p
className=""
data-pf-content={true}
>
<a
onClick={[Function]}
>
Return to the dashboard
</a>
</p>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <IssueComponent branding={brandingData} issue={issue} />;

expect(renderer.create(component).toJSON()).toMatchSnapshot();
});

it('should render an unknown error', () => {
const issue = {
type: 'unknown',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -32,6 +32,8 @@ export class IssueComponent extends React.PureComponent<Props> {
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);
}
Expand Down Expand Up @@ -92,6 +94,53 @@ export class IssueComponent extends React.PureComponent<Props> {
);
}

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 = (
<Text component={TextVariants.p}>
<a onClick={linkOnClick(workspaceRoutes.ideLoader)}>Restart your workspace</a>
</Text>
);

workspaceDetails = (
<Text component={TextVariants.p}>
<a onClick={linkOnClick(workspaceRoutes.workspaceDetails)}>Return to the dashboard</a>
</Text>
);
}

const warningTextbox = !error ? undefined : (
<Text component={TextVariants.pre} className={styles.errorMessage}>
{error.message}
</Text>
);

return (
<TextContent>
<Text component={TextVariants.h1}>
<WarningTriangleIcon className={styles.warningIcon} />
Warning
</Text>
{warningTextbox}
{ideLoader}
{workspaceDetails}
</TextContent>
);
}

private renderUnknownError(error: Error): React.ReactNode {
const errorTextbox = !error ? undefined : (
<Text component={TextVariants.pre} className={styles.errorMessage}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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');
});
});
11 changes: 11 additions & 0 deletions packages/dashboard-frontend/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
Expand Down
36 changes: 35 additions & 1 deletion packages/dashboard-frontend/src/services/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;
Copy link
Contributor Author

@dkwon17 dkwon17 Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if block is here as a dashboard side check for #556 (comment)

}

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 },
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

export enum SessionStorageKey {
PRIVATE_FACTORY_RELOADS = 'private-factory-reloads-number',
ORIGINAL_LOCATION_PATH = 'original-location-path',
}

export default class SessionStorageService {
Expand All @@ -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;
}
}