From 647d81352a3cad228bdfa5d50495edf1a1f7188b Mon Sep 17 00:00:00 2001 From: Vladyslav Zhukovskyi Date: Wed, 15 Jul 2020 16:15:22 +0300 Subject: [PATCH] Show recent Che workspaces in Theia editor Signed-off-by: Vladyslav Zhukovskyi --- che-theia-init-sources.yml | 1 + .../integration/theia/typescript.spec.ts | 2 +- .../eclipse-che-theia-workspace/.gitignore | 7 + .../eclipse-che-theia-workspace/package.json | 31 +++ .../src/browser/che-quick-open-workspace.ts | 203 ++++++++++++++++++ .../src/browser/che-workspace-contribution.ts | 50 +++++ .../src/browser/che-workspace-module.ts | 21 ++ .../eclipse-che-theia-workspace/tsconfig.json | 14 ++ 8 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 extensions/eclipse-che-theia-workspace/.gitignore create mode 100644 extensions/eclipse-che-theia-workspace/package.json create mode 100644 extensions/eclipse-che-theia-workspace/src/browser/che-quick-open-workspace.ts create mode 100644 extensions/eclipse-che-theia-workspace/src/browser/che-workspace-contribution.ts create mode 100644 extensions/eclipse-che-theia-workspace/src/browser/che-workspace-module.ts create mode 100644 extensions/eclipse-che-theia-workspace/tsconfig.json diff --git a/che-theia-init-sources.yml b/che-theia-init-sources.yml index af6e8f49a..5c84ea507 100644 --- a/che-theia-init-sources.yml +++ b/che-theia-init-sources.yml @@ -16,6 +16,7 @@ sources: - extensions/eclipse-che-theia-messaging - extensions/eclipse-che-theia-file-sync-tracker - extensions/eclipse-che-theia-cli-endpoint + - extensions/eclipse-che-theia-workspace plugins: - plugins/containers-plugin - plugins/workspace-plugin diff --git a/dockerfiles/theia/e2e/cypress/integration/theia/typescript.spec.ts b/dockerfiles/theia/e2e/cypress/integration/theia/typescript.spec.ts index bb666c5c2..bf8edd349 100644 --- a/dockerfiles/theia/e2e/cypress/integration/theia/typescript.spec.ts +++ b/dockerfiles/theia/e2e/cypress/integration/theia/typescript.spec.ts @@ -43,7 +43,7 @@ context('TypeScript', () => { // open /tmp cy.get('#theia-top-panel').should('exist').then(() => { - cy.theiaCommandPaletteClick('Open Workspace...', '{downarrow}').then(() => { + cy.theiaCommandPaletteClick('Open Workspace...', '{downarrow}{downarrow}').then(() => { cy.get('.theia-LocationList').should('exist'); cy.get('.theia-LocationList').select('file:///'); cy.wait(2000); diff --git a/extensions/eclipse-che-theia-workspace/.gitignore b/extensions/eclipse-che-theia-workspace/.gitignore new file mode 100644 index 000000000..ea3d8e091 --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/.gitignore @@ -0,0 +1,7 @@ +node_modules +.browser_modules +lib +*.log +*-app/* +!*-app/package.json +.idea diff --git a/extensions/eclipse-che-theia-workspace/package.json b/extensions/eclipse-che-theia-workspace/package.json new file mode 100644 index 000000000..97ad5d91a --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/package.json @@ -0,0 +1,31 @@ +{ + "name": "@eclipse-che/theia-workspace-extension", + "keywords": [ + "theia-extension" + ], + "version": "0.0.1", + "files": [ + "lib", + "src" + ], + "dependencies": { + "@eclipse-che/api": "latest", + "@theia/workspace": "next", + "@eclipse-che/theia-plugin-ext": "^0.0.1" + }, + "scripts": { + "prepare": "yarn clean && yarn build", + "clean": "rimraf lib", + "format": "tsfmt -r --useTsfmt ../../configs/tsfmt.json", + "lint": "eslint --cache=true --no-error-on-unmatched-pattern=true \"{src,test}/**/*.{ts,tsx}\"", + "compile": "tsc", + "build": "concurrently -n \"format,lint,compile\" -c \"red,green,blue\" \"yarn format\" \"yarn lint\" \"yarn compile\"", + "watch": "tsc -w" + }, + "license": "EPL-2.0", + "theiaExtensions": [ + { + "frontend": "lib/browser/che-workspace-module" + } + ] +} diff --git a/extensions/eclipse-che-theia-workspace/src/browser/che-quick-open-workspace.ts b/extensions/eclipse-che-theia-workspace/src/browser/che-quick-open-workspace.ts new file mode 100644 index 000000000..bffb11aac --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/src/browser/che-quick-open-workspace.ts @@ -0,0 +1,203 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +import { inject, injectable } from 'inversify'; +import { + QuickOpenGroupItem, + QuickOpenItem, + QuickOpenMode, + QuickOpenModel +} from '@theia/core/lib/common/quick-open-model'; +import { LabelProvider, QuickOpenService } from '@theia/core/lib/browser'; +import { CheApiService } from '@eclipse-che/theia-plugin-ext/lib/common/che-protocol'; +import { che as cheApi } from '@eclipse-che/api'; +import { OauthUtils } from '@eclipse-che/theia-plugin-ext/lib/browser/oauth-utils'; +import { AbstractDialog } from '@theia/core/lib/browser/dialogs'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import * as moment from 'moment'; +import { Message } from '@theia/core/lib/browser/widgets/index'; +import { Key } from '@theia/core/lib/browser/keyboard/keys'; + +@injectable() +export class QuickOpenCheWorkspace implements QuickOpenModel { + protected items: QuickOpenGroupItem[]; + protected currentWorkspace: cheApi.workspace.Workspace; + + @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService; + @inject(CheApiService) protected readonly cheApi: CheApiService; + @inject(OauthUtils) protected readonly oAuthUtils: OauthUtils; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + @inject(MessageService) protected readonly messageService: MessageService; + + private async open(workspaces: cheApi.workspace.Workspace[]): Promise { + this.items = []; + + if (!workspaces.length) { + this.items.push(new QuickOpenGroupItem({ + label: 'No Recent Workspaces', + run: (mode: QuickOpenMode): boolean => false + })); + return; + } + + for (const workspace of workspaces) { + const icon = this.labelProvider.folderIcon; + const iconClass = icon + ' file-icon'; + this.items.push(new QuickOpenGroupItem({ + label: this.getWorkspaceName(workspace) + (this.isCurrentWorkspace(workspace) ? ' (Current)' : ''), + detail: `Stack: ${this.getWorkspaceStack(workspace)}`, + groupLabel: `last modified ${moment(this.getWorkspaceModificationTime(workspace)).fromNow()}`, + iconClass, + run: (mode: QuickOpenMode): boolean => { + if (mode !== QuickOpenMode.OPEN) { + return false; + } + + if (this.isCurrentWorkspace(workspace)) { + return true; + } + + this.openWorkspace(workspace); + + return true; + }, + })); + } + + this.quickOpenService.open(this, { + placeholder: 'Type the name of the Che workspace you want to open', + fuzzyMatchLabel: true, + fuzzySort: false + }); + } + + onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void { + acceptor(this.items); + } + + async select(): Promise { + this.items = []; + + const token = await this.oAuthUtils.getUserToken(); + + if (!this.currentWorkspace) { + this.currentWorkspace = await this.cheApi.currentWorkspace(); + } + + if (!this.currentWorkspace.namespace) { + return; + } + + const workspaces = await this.cheApi.getAllByNamespace(this.currentWorkspace.namespace, token); + + workspaces.sort((a: cheApi.workspace.Workspace, b: cheApi.workspace.Workspace) => { + const updatedA: number = this.getWorkspaceModificationTime(a); + const updatedB: number = this.getWorkspaceModificationTime(b); + + if (isNaN(updatedA) || isNaN(updatedB)) { + return 0; + } else { + return updatedB - updatedA; + } + }); + + await this.open(workspaces); + } + + private getWorkspaceName(workspace: cheApi.workspace.Workspace): string | undefined { + if (workspace.devfile && workspace.devfile.metadata) { + return workspace.devfile.metadata.name; + } + } + + private getWorkspaceStack(workspace: cheApi.workspace.Workspace): string | undefined { + return workspace.attributes && workspace.attributes.stackName ? workspace.attributes.stackName : 'Custom'; + } + + private getWorkspaceModificationTime(workspace: cheApi.workspace.Workspace): number { + if (workspace.attributes) { + if (workspace.attributes.updated) { + return parseInt(workspace.attributes.updated); + } else if (workspace.attributes.created) { + return parseInt(workspace.attributes.created); + } + } + + return NaN; + } + + private stopCurrentWorkspace(): Promise { + class StopWorkspaceDialog extends AbstractDialog { + protected confirmed: boolean | undefined = true; + protected readonly dontStopButton: HTMLButtonElement; + + constructor() { + super({ + title: 'Open Workspace' + }); + + this.contentNode.appendChild(this.createMessageNode('Do you want to stop current workspace?')); + this.appendCloseButton('Cancel'); + this.dontStopButton = this.appendDontStopButton(); + this.appendAcceptButton('Yes'); + } + + get value(): boolean | undefined { + return this.confirmed; + } + + protected appendDontStopButton(): HTMLButtonElement { + const button = this.createButton('No'); + this.controlPanel.appendChild(button); + button.classList.add('secondary'); + return button; + } + + protected onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); + this.addKeyListener(this.dontStopButton, Key.ENTER, () => { + this.confirmed = false; + this.accept(); + }, 'click'); + } + + protected onCloseRequest(msg: Message): void { + super.onCloseRequest(msg); + this.confirmed = undefined; + this.accept(); + } + + protected createMessageNode(msg: string | HTMLElement): HTMLElement { + if (typeof msg === 'string') { + const messageNode = document.createElement('div'); + messageNode.textContent = msg; + return messageNode; + } + return msg; + } + + } + + return new StopWorkspaceDialog().open(); + } + + private async openWorkspace(workspace: cheApi.workspace.Workspace): Promise { + const result = await this.stopCurrentWorkspace(); + if (typeof result === 'boolean') { + if (result) { + await this.cheApi.stop(); + } + window.parent.postMessage(`open-workspace:${workspace.id}`, '*'); + } + } + + private isCurrentWorkspace(workspace: cheApi.workspace.Workspace): boolean { + return this.currentWorkspace.id === workspace.id; + } +} diff --git a/extensions/eclipse-che-theia-workspace/src/browser/che-workspace-contribution.ts b/extensions/eclipse-che-theia-workspace/src/browser/che-workspace-contribution.ts new file mode 100644 index 000000000..149a3fa3a --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/src/browser/che-workspace-contribution.ts @@ -0,0 +1,50 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +import { inject, injectable } from 'inversify'; +import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry } from '@theia/core/lib/common'; +import { CommonMenus } from '@theia/core/lib/browser'; +import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; +import { QuickOpenCheWorkspace } from './che-quick-open-workspace'; +import { Command } from '@theia/core/lib/common/command'; + +export namespace CheWorkspaceCommands { + + const FILE_CATEGORY = 'File'; + + export const OPEN_RECENT_WORKSPACE: Command = { + id: 'che.openRecentWorkspace', + category: FILE_CATEGORY, + label: 'Open Recent Workspace...' + }; +} + +@injectable() +export class CheWorkspaceContribution implements CommandContribution, MenuContribution { + + @inject(QuickOpenCheWorkspace) protected readonly quickOpenWorkspace: QuickOpenCheWorkspace; + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(CheWorkspaceCommands.OPEN_RECENT_WORKSPACE, { + execute: () => this.quickOpenWorkspace.select() + }); + } + + registerMenus(menus: MenuModelRegistry): void { + menus.unregisterMenuAction({ + commandId: WorkspaceCommands.OPEN_RECENT_WORKSPACE.id + }, CommonMenus.FILE_OPEN); + + menus.registerMenuAction(CommonMenus.FILE_OPEN, { + commandId: CheWorkspaceCommands.OPEN_RECENT_WORKSPACE.id, + label: CheWorkspaceCommands.OPEN_RECENT_WORKSPACE.label, + order: 'a20' + }); + } +} diff --git a/extensions/eclipse-che-theia-workspace/src/browser/che-workspace-module.ts b/extensions/eclipse-che-theia-workspace/src/browser/che-workspace-module.ts new file mode 100644 index 000000000..ba0243f16 --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/src/browser/che-workspace-module.ts @@ -0,0 +1,21 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +import { ContainerModule } from 'inversify'; +import { QuickOpenCheWorkspace } from './che-quick-open-workspace'; +import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; +import { CheWorkspaceContribution } from './che-workspace-contribution'; + +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(QuickOpenCheWorkspace).toSelf().inSingletonScope(); + bind(CheWorkspaceContribution).toSelf().inSingletonScope(); + for (const identifier of [CommandContribution, MenuContribution]) { + bind(identifier).toService(CheWorkspaceContribution); + } +}); diff --git a/extensions/eclipse-che-theia-workspace/tsconfig.json b/extensions/eclipse-che-theia-workspace/tsconfig.json new file mode 100644 index 000000000..f791d189e --- /dev/null +++ b/extensions/eclipse-che-theia-workspace/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../configs/base.tsconfig.json", + "compilerOptions": { + "lib": [ + "es6", + "dom" + ], + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ] +}