Skip to content

Commit 659889f

Browse files
mustard-mhjeanp413
authored andcommitted
[dashboard] support supervisor frontend server
Co-authored-by: mustard <[email protected]> Co-authored-by: Jean Pierre <[email protected]>
1 parent da841d0 commit 659889f

File tree

4 files changed

+285
-17
lines changed

4 files changed

+285
-17
lines changed

components/dashboard/src/service/service.tsx

Lines changed: 146 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,24 @@
55
*/
66

77
import {
8+
Emitter,
89
GitpodClient,
910
GitpodServer,
1011
GitpodServerPath,
1112
GitpodService,
1213
GitpodServiceImpl,
14+
User,
15+
WorkspaceInfo,
1316
} from "@gitpod/gitpod-protocol";
1417
import { WebSocketConnectionProvider } from "@gitpod/gitpod-protocol/lib/messaging/browser/connection";
15-
import { createWindowMessageConnection } from "@gitpod/gitpod-protocol/lib/messaging/browser/window-connection";
16-
import { JsonRpcProxyFactory } from "@gitpod/gitpod-protocol/lib/messaging/proxy-factory";
1718
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
1819
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
20+
import { IDEFrontendDashboardService } from "@gitpod/gitpod-protocol/lib/frontend-dashboard-service";
21+
import { RemoteTrackMessage } from "@gitpod/gitpod-protocol/lib/analytics";
1922

2023
export const gitpodHostUrl = new GitpodHostUrl(window.location.toString());
2124

2225
function createGitpodService<C extends GitpodClient, S extends GitpodServer>() {
23-
if (window.top !== window.self && process.env.NODE_ENV === "production") {
24-
const connection = createWindowMessageConnection("gitpodServer", window.parent, "*");
25-
const factory = new JsonRpcProxyFactory<S>();
26-
const proxy = factory.createProxy();
27-
factory.listen(connection);
28-
return new GitpodServiceImpl<C, S>(proxy, {
29-
onReconnect: async () => {
30-
await connection.sendRequest("$reconnectServer");
31-
},
32-
});
33-
}
3426
let host = gitpodHostUrl.asWebsocket().with({ pathname: GitpodServerPath }).withApi();
3527

3628
const connectionProvider = new WebSocketConnectionProvider();
@@ -53,7 +45,7 @@ function createGitpodService<C extends GitpodClient, S extends GitpodServer>() {
5345
return new GitpodServiceImpl<C, S>(proxy, { onReconnect });
5446
}
5547

56-
function getGitpodService(): GitpodService {
48+
export function getGitpodService(): GitpodService {
5749
const w = window as any;
5850
const _gp = w._gp || (w._gp = {});
5951
if (window.location.search.includes("service=mock")) {
@@ -64,4 +56,143 @@ function getGitpodService(): GitpodService {
6456
return service;
6557
}
6658

67-
export { getGitpodService };
59+
let ideFrontendService: IDEFrontendService | undefined;
60+
export function getIDEFrontendService(workspaceID: string, sessionId: string, service: GitpodService) {
61+
if (!ideFrontendService) {
62+
ideFrontendService = new IDEFrontendService(workspaceID, sessionId, service, window.parent);
63+
}
64+
return ideFrontendService;
65+
}
66+
67+
export class IDEFrontendService implements IDEFrontendDashboardService.IServer {
68+
private instanceID: string | undefined;
69+
private ideUrl: URL | undefined;
70+
private user: User | undefined;
71+
72+
private latestStatus?: IDEFrontendDashboardService.Status;
73+
74+
private readonly onDidChangeEmitter = new Emitter<IDEFrontendDashboardService.SetStateData>();
75+
readonly onSetState = this.onDidChangeEmitter.event;
76+
77+
constructor(
78+
private workspaceID: string,
79+
private sessionId: string,
80+
private service: GitpodService,
81+
private clientWindow: Window,
82+
) {
83+
this.processServerInfo();
84+
window.addEventListener("message", (event: MessageEvent) => {
85+
if (event.origin !== this.ideUrl?.origin) {
86+
return;
87+
}
88+
89+
if (IDEFrontendDashboardService.isTrackEventData(event.data)) {
90+
this.trackEvent(event.data.msg);
91+
}
92+
if (IDEFrontendDashboardService.isHeartbeatEventData(event.data)) {
93+
this.activeHeartbeat();
94+
}
95+
if (IDEFrontendDashboardService.isSetStateEventData(event.data)) {
96+
this.onDidChangeEmitter.fire(event.data.state);
97+
}
98+
});
99+
window.addEventListener("unload", () => {
100+
if (!this.instanceID) {
101+
return;
102+
}
103+
// send last heartbeat (wasClosed: true)
104+
const data = { sessionId: this.sessionId };
105+
const blob = new Blob([JSON.stringify(data)], { type: "application/json" });
106+
const gitpodHostUrl = new GitpodHostUrl(new URL(window.location.toString()));
107+
const url = gitpodHostUrl.withApi({ pathname: `/auth/workspacePageClose/${this.instanceID}` }).toString();
108+
navigator.sendBeacon(url, blob);
109+
});
110+
}
111+
112+
private async processServerInfo() {
113+
this.user = await this.service.server.getLoggedInUser();
114+
const workspace = await this.service.server.getWorkspace(this.workspaceID);
115+
this.instanceID = workspace.latestInstance?.id;
116+
if (this.instanceID) {
117+
this.auth();
118+
}
119+
120+
const listener = await this.service.listenToInstance(this.workspaceID);
121+
listener.onDidChange(() => {
122+
this.ideUrl = listener.info.latestInstance?.ideUrl
123+
? new URL(listener.info.latestInstance?.ideUrl)
124+
: undefined;
125+
const status = this.getWorkspaceStatus(listener.info);
126+
this.latestStatus = status;
127+
this.sendStatusUpdate(this.latestStatus);
128+
if (this.instanceID !== status.instanceId) {
129+
this.instanceID = status.instanceId;
130+
this.auth();
131+
}
132+
});
133+
}
134+
135+
getWorkspaceStatus(workspace: WorkspaceInfo): IDEFrontendDashboardService.Status {
136+
return {
137+
loggedUserId: this.user!.id,
138+
workspaceID: this.workspaceID,
139+
instanceId: workspace.latestInstance?.id,
140+
ideUrl: workspace.latestInstance?.ideUrl,
141+
statusPhase: workspace.latestInstance?.status.phase,
142+
workspaceDescription: workspace.workspace.description,
143+
workspaceType: workspace.workspace.type,
144+
};
145+
}
146+
147+
// implements
148+
149+
async auth() {
150+
if (!this.instanceID) {
151+
return;
152+
}
153+
const url = gitpodHostUrl.asWorkspaceAuth(this.instanceID).toString();
154+
await fetch(url, {
155+
credentials: "include",
156+
});
157+
}
158+
159+
trackEvent(msg: RemoteTrackMessage): void {
160+
msg.properties = {
161+
...msg.properties,
162+
sessionId: this.sessionId,
163+
instanceId: this.latestStatus?.instanceId,
164+
workspaceId: this.workspaceID,
165+
type: this.latestStatus?.workspaceType,
166+
};
167+
this.service.server.trackEvent(msg);
168+
}
169+
170+
activeHeartbeat(): void {
171+
if (this.instanceID) {
172+
this.service.server.sendHeartBeat({ instanceId: this.instanceID });
173+
}
174+
}
175+
176+
sendStatusUpdate(status: IDEFrontendDashboardService.Status): void {
177+
if (!this.ideUrl) {
178+
return;
179+
}
180+
this.clientWindow.postMessage(
181+
{
182+
type: "ide-status-update",
183+
status,
184+
} as IDEFrontendDashboardService.StatusUpdateEventData,
185+
this.ideUrl.origin,
186+
);
187+
}
188+
189+
relocate(url: string): void {
190+
if (!this.ideUrl) {
191+
return;
192+
}
193+
this.clientWindow.postMessage(
194+
{ type: "ide-relocate", url } as IDEFrontendDashboardService.RelocateEventData,
195+
this.ideUrl.origin,
196+
);
197+
}
198+
}

components/dashboard/src/start/StartWorkspace.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import Arrow from "../components/Arrow";
2525
import ContextMenu from "../components/ContextMenu";
2626
import PendingChangesDropdown from "../components/PendingChangesDropdown";
2727
import PrebuildLogs from "../components/PrebuildLogs";
28-
import { getGitpodService, gitpodHostUrl } from "../service/service";
28+
import { getGitpodService, gitpodHostUrl, getIDEFrontendService, IDEFrontendService } from "../service/service";
2929
import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage";
3030
import ConnectToSSHModal from "../workspaces/ConnectToSSHModal";
3131
import Alert from "../components/Alert";
@@ -103,6 +103,8 @@ export interface StartWorkspaceState {
103103
export default class StartWorkspace extends React.Component<StartWorkspaceProps, StartWorkspaceState> {
104104
static contextType = FeatureFlagContext;
105105

106+
private ideFrontendService: IDEFrontendService | undefined;
107+
106108
constructor(props: StartWorkspaceProps) {
107109
super(props);
108110
this.state = {};
@@ -111,6 +113,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
111113
private readonly toDispose = new DisposableCollection();
112114
componentWillMount() {
113115
if (this.props.runsInIFrame) {
116+
// TODO(hw): delete after supervisor deploy
114117
window.parent.postMessage({ type: "$setSessionId", sessionId }, "*");
115118
const setStateEventListener = (event: MessageEvent) => {
116119
if (
@@ -140,6 +143,23 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
140143
this.toDispose.push({
141144
dispose: () => window.removeEventListener("message", setStateEventListener),
142145
});
146+
// TODO(hw): end of delete
147+
148+
this.ideFrontendService = getIDEFrontendService(this.props.workspaceId, sessionId, getGitpodService());
149+
this.toDispose.push(
150+
this.ideFrontendService.onSetState((data) => {
151+
if (data.ideFrontendFailureCause) {
152+
const error = { message: data.ideFrontendFailureCause };
153+
this.setState({ error });
154+
}
155+
if (data.desktopIDE?.link) {
156+
const label = data.desktopIDE.label || "Open Desktop IDE";
157+
const clientID = data.desktopIDE.clientID;
158+
const link = data.desktopIDE?.link;
159+
this.setState({ desktopIde: { link, label, clientID } });
160+
}
161+
}),
162+
);
143163
}
144164

145165
try {
@@ -354,7 +374,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
354374
return;
355375
}
356376

357-
if (workspaceInstance.status.phase === "building" || workspaceInstance.status.phase == "preparing") {
377+
if (workspaceInstance.status.phase === "building" || workspaceInstance.status.phase === "preparing") {
358378
this.setState({ hasImageBuildLogs: true });
359379
}
360380

@@ -428,7 +448,10 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
428448

429449
redirectTo(url: string) {
430450
if (this.props.runsInIFrame) {
451+
// TODO(hw): delete after supervisor deploy
431452
window.parent.postMessage({ type: "relocate", url }, "*");
453+
// TODO(hw): end of delete
454+
this.ideFrontendService?.relocate(url);
432455
} else {
433456
window.location.href = url;
434457
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { WorkspaceInstancePhase } from "./workspace-instance";
8+
import { RemoteTrackMessage } from "./analytics";
9+
import { Event } from "./util/event";
10+
11+
export namespace IDEFrontendDashboardService {
12+
/**
13+
* IClient is the client side which is using in supervisor frontend
14+
*/
15+
export interface IClient extends IClientOn, IClientSend {}
16+
interface IClientOn {
17+
onStatusUpdate: Event<Status>;
18+
relocate(url: string): void;
19+
}
20+
interface IClientSend {
21+
trackEvent(msg: RemoteTrackMessage): void;
22+
activeHeartbeat(): void;
23+
setState(state: SetStateData): void;
24+
}
25+
26+
/**
27+
* IServer is the server side which is using in dashboard loading screen
28+
*/
29+
export interface IServer extends IServerOn, IServerSend {
30+
auth(): Promise<void>;
31+
}
32+
interface IServerOn {
33+
onSetState: Event<SetStateData>;
34+
trackEvent(msg: RemoteTrackMessage): void;
35+
activeHeartbeat(): void;
36+
}
37+
interface IServerSend {
38+
sendStatusUpdate(status: Status): void;
39+
relocate(url: string): void;
40+
}
41+
42+
export interface Status {
43+
workspaceID: string;
44+
loggedUserId: string;
45+
46+
instanceId?: string;
47+
ideUrl?: string;
48+
statusPhase?: WorkspaceInstancePhase;
49+
50+
workspaceDescription: string;
51+
workspaceType: string;
52+
}
53+
54+
export interface SetStateData {
55+
ideFrontendFailureCause?: string;
56+
desktopIDE?: {
57+
clientID: string;
58+
link: string;
59+
label?: string;
60+
};
61+
}
62+
63+
/**
64+
* interface for post message that send status update from dashboard to supervisor
65+
*/
66+
export interface StatusUpdateEventData {
67+
type: "ide-status-update";
68+
status: Status;
69+
}
70+
71+
export interface HeartbeatEventData {
72+
type: "ide-heartbeat";
73+
}
74+
75+
export interface TrackEventData {
76+
type: "ide-track-event";
77+
msg: RemoteTrackMessage;
78+
}
79+
80+
export interface RelocateEventData {
81+
type: "ide-relocate";
82+
url: string;
83+
}
84+
85+
export interface SetStateEventData {
86+
type: "ide-set-state";
87+
state: SetStateData;
88+
}
89+
90+
export function isStatusUpdateEventData(obj: any): obj is StatusUpdateEventData {
91+
return obj != null && typeof obj === "object" && obj.type === "ide-status-update";
92+
}
93+
94+
export function isHeartbeatEventData(obj: any): obj is HeartbeatEventData {
95+
return obj != null && typeof obj === "object" && obj.type === "ide-heartbeat";
96+
}
97+
98+
export function isTrackEventData(obj: any): obj is TrackEventData {
99+
return obj != null && typeof obj === "object" && obj.type === "ide-track-event";
100+
}
101+
102+
export function isRelocateEventData(obj: any): obj is RelocateEventData {
103+
return obj != null && typeof obj === "object" && obj.type === "ide-relocate";
104+
}
105+
106+
export function isSetStateEventData(obj: any): obj is SetStateEventData {
107+
return obj != null && typeof obj === "object" && obj.type === "ide-set-state";
108+
}
109+
}

components/gitpod-protocol/src/util/gitpod-host-url.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ export class GitpodHostUrl {
169169
return pathSegs[2];
170170
}
171171

172+
const cleanHash = this.url.hash.replace(/^#/, "");
173+
if (this.url.pathname == "/start/" && cleanHash.match(workspaceIDRegex)) {
174+
return cleanHash;
175+
}
176+
172177
return undefined;
173178
}
174179

0 commit comments

Comments
 (0)