Path: blob/main/components/supervisor/frontend/src/index.ts
2500 views
/**1* Copyright (c) 2020 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56/**7* <script type="text/javascript" src="/_supervisor/frontend/main.js" charset="utf-8"></script> should be inserted to index.html as first body script,8* all other IDE scripts should go afterwards, head element should not have scripts9*/1011import { IDEMetricsServiceClient, MetricsName } from "./ide/ide-metrics-service-client";12IDEMetricsServiceClient.addCounter(MetricsName.SupervisorFrontendClientTotal).catch(() => {});1314//#region supervisor frontend error capture15function isElement(obj: any): obj is Element {16return typeof obj.getAttribute === "function";17}1819window.addEventListener("error", (event) => {20const labels: Record<string, string> = {};21let resourceSource: string | null | undefined;22if (isElement(event.target)) {23// We take a look at what is the resource that was attempted to load;24resourceSource = event.target.getAttribute("src") || event.target.getAttribute("href");25// If the event has a `target`, it means that it wasn't a script error26if (resourceSource) {27if (resourceSource.match(new RegExp(/\/build\/ide\/code:.+\/__files__\//g))) {28// TODO(ak) reconsider how to hide knowledge of VS Code from supervisor frontend, i.e instrument amd loader instead29labels["resource"] = "vscode-web-workbench";30}31labels["error"] = "LoadError";32}33}34if (event.error) {35IDEMetricsServiceClient.reportError(event.error).catch(() => {});36} else if (labels["error"] == "LoadError") {37let error = new Error("LoadError");38IDEMetricsServiceClient.reportError(error, {39resource: labels["resource"],40url: resourceSource ?? "",41}).catch(() => {});42}43IDEMetricsServiceClient.addCounter(MetricsName.SupervisorFrontendErrorTotal, labels).catch(() => {});44});45//#endregion4647require("../src/shared/index.css");4849import { WorkspaceInstancePhase } from "@gitpod/gitpod-protocol";50import { DisposableCollection } from "@gitpod/gitpod-protocol/lib/util/disposable";51import * as heartBeat from "./ide/heart-beat";52import * as IDEFrontendService from "./ide/ide-frontend-service-impl";53import * as IDEWorker from "./ide/ide-worker";54import * as IDEWebSocket from "./ide/ide-web-socket";55import { SupervisorServiceClient } from "./ide/supervisor-service-client";56import * as LoadingFrame from "./shared/loading-frame";57import { workspaceUrl } from "./shared/urls";5859window.gitpod = {} as any;60IDEWorker.install();61IDEWebSocket.install();62const ideService = IDEFrontendService.create();63const loadingIDE = new Promise((resolve) => window.addEventListener("DOMContentLoaded", resolve, { once: true }));64const toStop = new DisposableCollection();65let willRedirect = false;6667document.body.style.visibility = "hidden";68LoadingFrame.load().then(async (loading) => {69const frontendDashboardServiceClient = loading.frontendDashboardServiceClient;70await frontendDashboardServiceClient.initialize();7172if (frontendDashboardServiceClient.latestInfo.workspaceType !== "regular") {73return;74}7576frontendDashboardServiceClient.onWillRedirect(() => {77willRedirect = true;78});7980document.title = frontendDashboardServiceClient.latestInfo.workspaceDescription ?? "gitpod";81window.gitpod.loggedUserID = frontendDashboardServiceClient.latestInfo.loggedUserId;82window.gitpod.openDesktopIDE = frontendDashboardServiceClient.openDesktopIDE.bind(frontendDashboardServiceClient);83window.gitpod.decrypt = frontendDashboardServiceClient.decrypt.bind(frontendDashboardServiceClient);84window.gitpod.encrypt = frontendDashboardServiceClient.encrypt.bind(frontendDashboardServiceClient);85window.gitpod.isEncryptedData = frontendDashboardServiceClient.isEncryptedData.bind(frontendDashboardServiceClient);8687const supervisorServiceClient = new SupervisorServiceClient(frontendDashboardServiceClient);8889(async () => {90let hideDesktopIde = false;91const hideDesktopIdeEventListener = frontendDashboardServiceClient.onOpenBrowserIDE(() => {92hideDesktopIdeEventListener.dispose();93hideDesktopIde = true;94toStop.push(ideService.start());95});96toStop.push(hideDesktopIdeEventListener);9798//#region gitpod browser telemetry99// TODO(ak) get rid of it100// it is bad usage of window.postMessage101// VS Code should use Segment directly here and publish to production/staging untrusted102// supervisor frontend should not care about IDE specifics103window.addEventListener("message", async (event) => {104const type = event.data.type;105if (type === "vscode_telemetry") {106const { event: eventName, properties } = event.data;107frontendDashboardServiceClient.trackEvent({108event: eventName,109properties,110});111}112});113//#endregion114115type DesktopIDEStatus = { link: string; label: string; clientID?: string; kind?: String };116let isDesktopIde: undefined | boolean = undefined;117let ideStatus: undefined | { desktop: DesktopIDEStatus } = undefined;118119//#region current-frame120let current: HTMLElement = loading.frame;121let desktopRedirected = false;122let currentInstanceId = "";123const nextFrame = () => {124const { instanceId, ideUrl, statusPhase } = frontendDashboardServiceClient.latestInfo ?? {};125126if (instanceId) {127// refresh web page when instanceId changed128if (currentInstanceId !== "") {129if (instanceId !== currentInstanceId && ideUrl !== "") {130currentInstanceId = instanceId;131window.location.href = ideUrl!;132}133} else {134currentInstanceId = instanceId;135}136if (statusPhase === "running") {137if (!hideDesktopIde) {138if (isDesktopIde == undefined) {139return loading.frame;140}141if (isDesktopIde && !!ideStatus) {142trackDesktopIDEReady(ideStatus.desktop);143frontendDashboardServiceClient.setState({144desktopIDE: {145link: ideStatus.desktop.link,146label: ideStatus.desktop.label || "Open Desktop IDE",147clientID: ideStatus.desktop.clientID!,148},149});150if (!desktopRedirected) {151desktopRedirected = true;152frontendDashboardServiceClient.openDesktopIDE(ideStatus.desktop.link);153}154return loading.frame;155}156}157if (ideService.state === "ready") {158return document.body;159}160}161}162return loading.frame;163};164const updateCurrentFrame = () => {165const newCurrent = nextFrame();166if (current === newCurrent || willRedirect) {167return;168}169current.style.visibility = "hidden";170newCurrent.style.visibility = "visible";171if (current === document.body) {172while (document.body.firstChild && document.body.firstChild !== newCurrent) {173document.body.removeChild(document.body.firstChild);174}175while (document.body.lastChild && document.body.lastChild !== newCurrent) {176document.body.removeChild(document.body.lastChild);177}178}179current = newCurrent;180};181182const updateLoadingState = () => {183frontendDashboardServiceClient.setState({184ideFrontendFailureCause: ideService.failureCause?.message,185});186};187const trackStatusRenderedEvent = (188phase: string,189properties?: {190[prop: string]: any;191},192) => {193frontendDashboardServiceClient.trackEvent({194event: "status_rendered",195properties: {196phase,197...properties,198},199});200};201let trackedDesktopIDEReady = false;202const trackDesktopIDEReady = ({ clientID, kind }: DesktopIDEStatus) => {203if (trackedDesktopIDEReady) {204return;205}206trackedDesktopIDEReady = true;207trackStatusRenderedEvent("desktop-ide-ready", { clientID, kind });208};209const trackIDEStatusRenderedEvent = () => {210let error: string | undefined;211if (ideService.failureCause) {212error = `${ideService.failureCause.message}\n${ideService.failureCause.stack}`;213}214trackStatusRenderedEvent(`ide-${ideService.state}`, { error });215};216217updateCurrentFrame();218updateLoadingState();219trackIDEStatusRenderedEvent();220frontendDashboardServiceClient.onInfoUpdate(() => updateCurrentFrame());221ideService.onDidChange(() => {222updateLoadingState();223updateCurrentFrame();224trackIDEStatusRenderedEvent();225});226supervisorServiceClient.ideReady227.then((newIdeStatus) => {228ideStatus = newIdeStatus;229isDesktopIde = !!ideStatus && !!ideStatus.desktop && !!ideStatus.desktop.link;230updateCurrentFrame();231})232.catch((error) => console.error(`Unexpected error from supervisorServiceClient.ideReady: ${error}`));233window.addEventListener("unload", () => trackStatusRenderedEvent("window-unload"), { capture: true });234//#endregion235236//#region heart-beat237heartBeat.track(window);238let isOwner = false;239supervisorServiceClient.getWorkspaceInfoPromise.then((info) => {240isOwner = frontendDashboardServiceClient.latestInfo.loggedUserId === info.ownerId;241updateHeartBeat();242});243const updateHeartBeat = () => {244if (frontendDashboardServiceClient.latestInfo?.statusPhase === "running" && isOwner) {245heartBeat.schedule(frontendDashboardServiceClient);246} else {247heartBeat.cancel();248}249};250updateHeartBeat();251frontendDashboardServiceClient.onInfoUpdate(() => updateHeartBeat());252//#endregion253})();254255(async () => {256const debugWorkspace = workspaceUrl.debugWorkspace;257//#region ide lifecycle258function isWorkspaceInstancePhase(phase: WorkspaceInstancePhase): boolean {259return frontendDashboardServiceClient.latestInfo?.statusPhase === phase;260}261if (!isWorkspaceInstancePhase("running")) {262if (debugWorkspace && frontendDashboardServiceClient.latestInfo) {263window.open("", "_self")?.close();264}265await new Promise<void>((resolve) => {266frontendDashboardServiceClient.onInfoUpdate((status) => {267if (status.statusPhase === "running") {268resolve();269}270});271});272}273if (debugWorkspace) {274supervisorServiceClient.supervisorWillShutdown.then(() => {275window.open("", "_self")?.close();276});277}278const [ideStatus] = await Promise.all([279supervisorServiceClient.ideReady,280supervisorServiceClient.contentReady,281loadingIDE,282]);283if (isWorkspaceInstancePhase("stopping") || isWorkspaceInstancePhase("stopped")) {284return;285}286toStop.pushAll([287IDEWebSocket.connectWorkspace(),288frontendDashboardServiceClient.onInfoUpdate((status) => {289if (status.statusPhase === "stopping" || status.statusPhase === "stopped") {290toStop.dispose();291}292}),293]);294const isDesktopIde = ideStatus && ideStatus.desktop && ideStatus.desktop.link;295if (!isDesktopIde) {296toStop.push(ideService.start());297}298//#endregion299})();300});301302303