Skip to content
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
49 changes: 31 additions & 18 deletions components/dashboard/src/service/service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export function getIDEFrontendService(workspaceID: string, sessionId: string, se

export class IDEFrontendService implements IDEFrontendDashboardService.IServer {
private instanceID: string | undefined;
private ideUrl: URL | undefined;
private user: User | undefined;

private latestStatus?: IDEFrontendDashboardService.Status;
Expand All @@ -82,10 +81,6 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer {
) {
this.processServerInfo();
window.addEventListener("message", (event: MessageEvent) => {
if (event.origin !== this.ideUrl?.origin) {
return;
}

if (IDEFrontendDashboardService.isTrackEventData(event.data)) {
this.trackEvent(event.data.msg);
}
Expand All @@ -95,6 +90,9 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer {
if (IDEFrontendDashboardService.isSetStateEventData(event.data)) {
this.onDidChangeEmitter.fire(event.data.state);
}
if (IDEFrontendDashboardService.isOpenDesktopIDE(event.data)) {
this.openDesktopIDE(event.data.url);
}
});
window.addEventListener("unload", () => {
if (!this.instanceID) {
Expand All @@ -116,7 +114,6 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer {
const reconcile = () => {
const status = this.getWorkspaceStatus(listener.info);
this.latestStatus = status;
this.ideUrl = status.ideUrl ? new URL(status.ideUrl) : undefined;
const oldInstanceID = this.instanceID;
this.instanceID = status.instanceId;
if (status.instanceId && oldInstanceID !== status.instanceId) {
Expand All @@ -128,7 +125,7 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer {
listener.onDidChange(reconcile);
}

getWorkspaceStatus(workspace: WorkspaceInfo): IDEFrontendDashboardService.Status {
private getWorkspaceStatus(workspace: WorkspaceInfo): IDEFrontendDashboardService.Status {
return {
loggedUserId: this.user!.id,
workspaceID: this.workspaceID,
Expand All @@ -142,7 +139,7 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer {

// implements

async auth() {
private async auth() {
if (!this.instanceID) {
return;
}
Expand All @@ -152,7 +149,7 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer {
});
}

trackEvent(msg: RemoteTrackMessage): void {
private trackEvent(msg: RemoteTrackMessage): void {
msg.properties = {
...msg.properties,
sessionId: this.sessionId,
Expand All @@ -163,32 +160,48 @@ export class IDEFrontendService implements IDEFrontendDashboardService.IServer {
this.service.server.trackEvent(msg);
}

activeHeartbeat(): void {
private activeHeartbeat(): void {
if (this.instanceID) {
this.service.server.sendHeartBeat({ instanceId: this.instanceID });
}
}

sendStatusUpdate(status: IDEFrontendDashboardService.Status): void {
if (!this.ideUrl) {
return;
openDesktopIDE(url: string): void {
let redirect = false;
try {
const desktopLink = new URL(url);
redirect = desktopLink.protocol !== "http:" && desktopLink.protocol !== "https:";
} catch (e) {
console.error("invalid desktop link:", e);
}
// redirect only if points to desktop application
// don't navigate browser to another page
if (redirect) {
window.location.href = url;
} else {
window.open(url, "_blank", "noopener");
}
}

sendStatusUpdate(status: IDEFrontendDashboardService.Status): void {
this.clientWindow.postMessage(
{
version: 1,
type: "ide-status-update",
status,
} as IDEFrontendDashboardService.StatusUpdateEventData,
this.ideUrl.origin,
"*",
);
}

relocate(url: string): void {
if (!this.ideUrl) {
return;
}
this.clientWindow.postMessage(
{ type: "ide-relocate", url } as IDEFrontendDashboardService.RelocateEventData,
this.ideUrl.origin,
"*",
);
}

openBrowserIDE(): void {
this.clientWindow.postMessage({ type: "ide-open-browser" } as IDEFrontendDashboardService.OpenBrowserIDE, "*");
}
}
23 changes: 8 additions & 15 deletions components/dashboard/src/start/StartWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -458,20 +458,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
}

private openDesktopLink(link: string) {
let redirect = false;
try {
const desktopLink = new URL(link);
redirect = desktopLink.protocol !== "http:" && desktopLink.protocol !== "https:";
} catch (e) {
console.error("invalid desktop link:", e);
}
// redirect only if points to desktop application
// don't navigate browser to another page
if (redirect) {
window.location.href = link;
} else {
window.open(link, "_blank", "noopener");
}
this.ideFrontendService?.openDesktopIDE(link);
}

render() {
Expand Down Expand Up @@ -578,7 +565,13 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
menuEntries={[
{
title: "Open in Browser",
onClick: () => window.parent.postMessage({ type: "openBrowserIde" }, "*"),
onClick: () => {
// TODO(ak): delete after supervisor deploy
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this TODO intended to be left in place?

Copy link
Member Author

Choose a reason for hiding this comment

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

after both deployments, we will do another PR to clean up

window.parent.postMessage({ type: "openBrowserIde" }, "*");
// TODO(ak): end of delete

this.ideFrontendService?.openBrowserIDE();
},
},
{
title: "Stop Workspace",
Expand Down
48 changes: 32 additions & 16 deletions components/gitpod-protocol/src/frontend-dashboard-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,34 @@

import { WorkspaceInstancePhase } from "./workspace-instance";
import { RemoteTrackMessage } from "./analytics";
import { Event } from "./util/event";

/**
* IDEFrontendDashboardService enables IDE related communications
* between the workspace to gitpod origins. Use it to communicate
* between IDE and dashboard iframes, never use
* `window.postMessage` directly.
*
* **Security Note**: never expose information about other workspaces
* or sensitive information for the current workspace, i.e. owner token.
*/
export namespace IDEFrontendDashboardService {
/**
* IClient is the client side which is using in supervisor frontend
*/
export interface IClient extends IClientOn, IClientSend {}
interface IClientOn {
onStatusUpdate: Event<Status>;
relocate(url: string): void;
}
interface IClientSend {
export interface IClient {
trackEvent(msg: RemoteTrackMessage): void;
activeHeartbeat(): void;
setState(state: SetStateData): void;
openDesktopIDE(url: string): void;
}

/**
* IServer is the server side which is using in dashboard loading screen
*/
export interface IServer extends IServerOn, IServerSend {
auth(): Promise<void>;
}
interface IServerOn {
onSetState: Event<SetStateData>;
trackEvent(msg: RemoteTrackMessage): void;
activeHeartbeat(): void;
}
interface IServerSend {
export interface IServer {
sendStatusUpdate(status: Status): void;
relocate(url: string): void;
openBrowserIDE(): void;
}

export interface Status {
Expand Down Expand Up @@ -64,6 +61,8 @@ export namespace IDEFrontendDashboardService {
* interface for post message that send status update from dashboard to supervisor
*/
export interface StatusUpdateEventData {
// protocol version
version?: number;
type: "ide-status-update";
status: Status;
}
Expand All @@ -87,6 +86,15 @@ export namespace IDEFrontendDashboardService {
state: SetStateData;
}

export interface OpenBrowserIDE {
type: "ide-open-browser";
}

export interface OpenDesktopIDE {
type: "ide-open-desktop";
url: string;
}

export function isStatusUpdateEventData(obj: any): obj is StatusUpdateEventData {
return obj != null && typeof obj === "object" && obj.type === "ide-status-update";
}
Expand All @@ -106,4 +114,12 @@ export namespace IDEFrontendDashboardService {
export function isSetStateEventData(obj: any): obj is SetStateEventData {
return obj != null && typeof obj === "object" && obj.type === "ide-set-state";
}

export function isOpenBrowserIDE(obj: any): obj is OpenBrowserIDE {
return obj != null && typeof obj === "object" && obj.type === "ide-open-browser";
}

export function isOpenDesktopIDE(obj: any): obj is OpenDesktopIDE {
return obj != null && typeof obj === "object" && obj.type === "ide-open-desktop";
}
}
18 changes: 7 additions & 11 deletions components/supervisor/frontend/src/regular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,12 @@ LoadingFrame.load().then(async (loading) => {
const supervisorServiceClient = SupervisorServiceClient.get();

let hideDesktopIde = false;
const serverOrigin = startUrl.url.origin;
const hideDesktopIdeEventListener = (event: MessageEvent) => {
if (event.origin === serverOrigin && event.data.type == "openBrowserIde") {
window.removeEventListener("message", hideDesktopIdeEventListener);
hideDesktopIde = true;
toStop.push(ideService.start());
}
};
window.addEventListener("message", hideDesktopIdeEventListener, false);
toStop.push({ dispose: () => window.removeEventListener("message", hideDesktopIdeEventListener) });
const hideDesktopIdeEventListener = frontendDashboardServiceClient.onOpenBrowserIDE(() => {
hideDesktopIdeEventListener.dispose();
hideDesktopIde = true;
toStop.push(ideService.start());
});
toStop.push(hideDesktopIdeEventListener);

//#region gitpod browser telemetry
// TODO(ak) get rid of it
Expand Down Expand Up @@ -140,7 +136,7 @@ LoadingFrame.load().then(async (loading) => {
});
if (!desktopRedirected) {
desktopRedirected = true;
loading.openDesktopLink(ideStatus.desktop.link);
frontendDashboardServiceClient.openDesktopIDE(ideStatus.desktop.link);
}
return loading.frame;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,37 @@ export class FrontendDashboardServiceClient implements IDEFrontendDashboardServi
private readonly onDidChangeEmitter = new Emitter<IDEFrontendDashboardService.Status>();
readonly onStatusUpdate = this.onDidChangeEmitter.event;

private readonly onOpenBrowserIDEEmitter = new Emitter<void>();
readonly onOpenBrowserIDE = this.onOpenBrowserIDEEmitter.event;

private resolveInit!: () => void;
private initPromise = new Promise<void>((resolve) => (this.resolveInit = resolve));

private version?: number;

constructor(private serverWindow: Window) {
window.addEventListener("message", (event: MessageEvent) => {
if (event.origin !== serverUrl.url.origin) {
return;
}
if (IDEFrontendDashboardService.isStatusUpdateEventData(event.data)) {
this.version = event.data.version;
this.latestStatus = event.data.status;
this.resolveInit();
this.onDidChangeEmitter.fire(this.latestStatus);
}
if (IDEFrontendDashboardService.isRelocateEventData(event.data)) {
this.relocate(event.data.url);
window.location.href = event.data.url;
}
if (IDEFrontendDashboardService.isOpenBrowserIDE(event.data)) {
this.onOpenBrowserIDEEmitter.fire(undefined);
}
});
}

initialize(): Promise<void> {
return this.initPromise;
}

relocate(url: string): void {
window.location.href = url;
}

trackEvent(msg: RemoteTrackMessage): void {
this.serverWindow.postMessage(
{ type: "ide-track-event", msg } as IDEFrontendDashboardService.TrackEventData,
Expand All @@ -62,4 +66,32 @@ export class FrontendDashboardServiceClient implements IDEFrontendDashboardServi
serverUrl.url.origin,
);
}

openDesktopIDE(url: string): void {
if (this.version && this.version >= 1) {
// always perfrom redirect to dekstop IDE on gitpod origin
// to avoid confirmation popup on each workspace origin
this.serverWindow.postMessage(
{ type: "ide-open-desktop", url } as IDEFrontendDashboardService.OpenDesktopIDE,
serverUrl.url.origin,
);
return;
}

// TODO(ak) remove after new dashboard is deployed
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question about the todo

let redirect = false;
try {
const desktopLink = new URL(url);
redirect = desktopLink.protocol !== "http:" && desktopLink.protocol !== "https:";
} catch (e) {
console.error("invalid desktop link:", e);
}
// redirect only if points to desktop application
// don't navigate browser to another page
if (redirect) {
window.location.href = url;
} else {
window.open(url, "_blank", "noopener");
}
}
}
19 changes: 1 addition & 18 deletions components/supervisor/frontend/src/shared/loading-frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { startUrl } from "./urls";
export function load(): Promise<{
frame: HTMLIFrameElement;
frontendDashboardServiceClient: FrontendDashboardServiceClient;
openDesktopLink: (link: string) => void;
}> {
return new Promise((resolve) => {
const frame = document.createElement("iframe");
Expand All @@ -21,23 +20,7 @@ export function load(): Promise<{

frame.onload = () => {
const frontendDashboardServiceClient = new FrontendDashboardServiceClient(frame.contentWindow!);
const openDesktopLink = (link: string) => {
let redirect = false;
try {
const desktopLink = new URL(link);
redirect = desktopLink.protocol !== "http:" && desktopLink.protocol !== "https:";
} catch (e) {
console.error("invalid desktop link:", e);
}
// redirect only if points to desktop application
// don't navigate browser to another page
if (redirect) {
window.location.href = link;
} else {
window.open(link, "_blank", "noopener");
}
};
resolve({ frame, frontendDashboardServiceClient, openDesktopLink });
resolve({ frame, frontendDashboardServiceClient });
};
});
}