diff --git a/deadlock-plugins/deadlock-extension/.vscode/launch.json b/deadlock-plugins/deadlock-extension/.vscode/launch.json index 44ec2a1b7c4c09cb5626d28802fa3272b7862365..71f1eaee1da467cf15e99ba14b73062132c45f8f 100644 --- a/deadlock-plugins/deadlock-extension/.vscode/launch.json +++ b/deadlock-plugins/deadlock-extension/.vscode/launch.json @@ -20,7 +20,9 @@ "args": ["--extensionDevelopmentPath=${workspaceFolder}"], "outFiles": ["${workspaceFolder}/dist/**/*.js"], "env": { - "DL_MOUNT_EXTENSION": "true" + "DL_MOUNT_EXTENSION": "true", + // TODO: SETTING: replace with your own extension path + "EXTENSION_PATH": "${workspaceFolder}/deadlock-coding-0.1.10.vsix" }, "preLaunchTask": "${defaultBuildTask}" }, @@ -33,7 +35,7 @@ "env": { "DL_MOUNT_EXTENSION": "true", // TODO: SETTING: replace with your own extension path - "EXTENSION_PATH": "/home/takima/Desktop/d-projects/deadlock-desktop/deadlock-plugins/deadlock-extension/deadlock-coding-0.1.10.vsix" + "EXTENSION_PATH": "${workspaceFolder}/deadlock-coding-0.1.10.vsix" }, "preLaunchTask": "tasks: build and watch" }, diff --git a/deadlock-plugins/deadlock-extension/.vscodeignore b/deadlock-plugins/deadlock-extension/.vscodeignore index 62bf20897c27705b453c85d440e97fb4f0d9bc2a..358029d704b76e53507a98a2ae97de7d33de94bd 100644 --- a/deadlock-plugins/deadlock-extension/.vscodeignore +++ b/deadlock-plugins/deadlock-extension/.vscodeignore @@ -4,4 +4,3 @@ out/ src/ dev/ tsconfig.json -node_modules \ No newline at end of file diff --git a/deadlock-plugins/deadlock-extension/package.json b/deadlock-plugins/deadlock-extension/package.json index badb37739425fa8925e11efa2fb924dbebd86dd6..bd2be05fb0d82950b6d6d0e3d739ee55c61844f9 100644 --- a/deadlock-plugins/deadlock-extension/package.json +++ b/deadlock-plugins/deadlock-extension/package.json @@ -59,16 +59,22 @@ "when": "deadlock.inContainer" }, { - "id": "help", - "name": "Help", - "visibility": "visible" + "id": "startedMissions", + "type": "webview", + "name": "Started Missions", + "contextualTitle": "Deadlock", + "when": "!deadlock.inContainer" }, { "id": "commandTree", "name": "Commands", - "icon": "media/dep.svg", "contextualTitle": "Deadlock", "when": "deadlock.inContainer" + }, + { + "id": "help", + "name": "Help", + "visibility": "visible" } ] }, diff --git a/deadlock-plugins/deadlock-extension/resources/js/startedMissionsView.js b/deadlock-plugins/deadlock-extension/resources/js/startedMissionsView.js new file mode 100644 index 0000000000000000000000000000000000000000..413890dc390885265b4f4df95454bb4bde693a43 --- /dev/null +++ b/deadlock-plugins/deadlock-extension/resources/js/startedMissionsView.js @@ -0,0 +1,8 @@ +const vscode = acquireVsCodeApi(); + +function openMission(mission) { + vscode.postMessage({ + command: 'openMission', + mission: mission, + }); +} diff --git a/deadlock-plugins/deadlock-extension/resources/styles/startedMissionsView.css b/deadlock-plugins/deadlock-extension/resources/styles/startedMissionsView.css new file mode 100644 index 0000000000000000000000000000000000000000..2ad5394eda129f3a5291d7289bd9703d9cbd7f55 --- /dev/null +++ b/deadlock-plugins/deadlock-extension/resources/styles/startedMissionsView.css @@ -0,0 +1,6 @@ +.item { + width: 100%; + margin: 1em 0em 0em 0em; + word-wrap: normal; + justify-content: center; +} \ No newline at end of file diff --git a/deadlock-plugins/deadlock-extension/src/core/api.service.ts b/deadlock-plugins/deadlock-extension/src/core/api.service.ts index 7087949b9710fbdbfc01947357852b9f71fd19c4..90a1716d0df33c1d0d8641150bd502b3a519ff56 100644 --- a/deadlock-plugins/deadlock-extension/src/core/api.service.ts +++ b/deadlock-plugins/deadlock-extension/src/core/api.service.ts @@ -2,11 +2,11 @@ import axios, { AxiosError, AxiosInstance } from 'axios'; import { API_QUERY_REFERER, API_URL } from '../config'; import { GiteaPublicProperties } from '../model/giteaPublicProperties.model'; import { SshKeyPair } from '../model/sshKeyPair.model'; -import { User } from '../model/user.model'; import { extensionError as error } from '../recorder/utils/log'; import Controller from './controller'; import ExtensionStore from './extensionStore'; import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection'; +import { User } from './mission/models/userChallenge'; export default class ApiService { private axiosInstance: AxiosInstance; @@ -128,7 +128,7 @@ export default class ApiService { return res.data; } - async getUser(userId: string | null): Promise<User> { + async getUser(userId?: string): Promise<User> { if (!userId) return this.getCurrentUser(); const res = await this.axiosInstance.get<User>(`users/${userId}`); return res.data; diff --git a/deadlock-plugins/deadlock-extension/src/core/controller.ts b/deadlock-plugins/deadlock-extension/src/core/controller.ts index bd342ee76dc9332fda2d60c7df1e049aa5315247..b93c6060deb7a59f8701dc8292636c907064819f 100644 --- a/deadlock-plugins/deadlock-extension/src/core/controller.ts +++ b/deadlock-plugins/deadlock-extension/src/core/controller.ts @@ -6,15 +6,16 @@ import ExtensionStore from './extensionStore'; import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection'; import ApiService from './api.service'; import { GiteaPublicProperties } from '../model/giteaPublicProperties.model'; -import { User } from '../model/user.model'; -import { Mission } from './mission/mission'; +import { Mission } from './mission/models/mission'; import { MissionDevContainer } from './mission/missionDevContainer'; import { extensionLog as log } from '../recorder/utils/log'; import { commands, ExtensionContext, Uri, window } from 'vscode'; import { createSshKeyFiles, isSshKeyPairExist } from './sshKeyManager'; import { hasStatusNumber } from './utils/typeguards'; +import { User } from './mission/models/userChallenge'; export default class Controller { + private static instance: Controller; public connection: KeycloakOAuth2DeviceFlowConnection; private commandHandler: CommandHandler; private briefingView: BriefingView; @@ -22,7 +23,7 @@ export default class Controller { private extensionStore: ExtensionStore; private apiService: ApiService; - constructor(private context: ExtensionContext) { + private constructor(context: ExtensionContext) { this.extensionStore = ExtensionStore.getInstance(context); this.briefingView = new BriefingView(); this.quickSetupView = new QuickSetupView(context.extensionUri); @@ -38,6 +39,13 @@ export default class Controller { this.init(); } + public static getInstance(context: ExtensionContext): Controller { + if (!Controller.instance) { + Controller.instance = new Controller(context); + } + return Controller.instance; + } + private async init() { const that = this; window.registerUriHandler({ @@ -47,7 +55,7 @@ export default class Controller { // TODO: Should we follow eslint no-case-declarations ? const missionId = queryParams.get('missionId'); const missionVersion = queryParams.get('missionVersion'); - const userId = queryParams.get('userId'); + const userId = queryParams.get('userId') ?? undefined; log('Opening link', uri); switch (action) { @@ -113,7 +121,7 @@ export default class Controller { commands.executeCommand(openUrlInBrowserCommand.cmd, Uri.parse(url)); } - public async launchMission(missionId: string, missionVersion: string, userId: string | null) { + public async launchMission(missionId: string, missionVersion: string, userId?: string) { window.showInformationMessage(`vous lancez la mission ${missionId}`); const hadMissionWorkdir = this.extensionStore.getMissionWorkdir() !== undefined; if (!hadMissionWorkdir) { diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts b/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts index 8350f2abb651049db4faeaa6b70f4f84a098742d..cccd611ddb70fa4bbab305d7662fd5ab4454427e 100644 --- a/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts +++ b/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts @@ -1,14 +1,14 @@ import { userSshKeyFolderPath } from '../config'; -import { Base, DockerfileSpecific, LifecycleScripts, VSCodespecific } from './devContainer'; +import { Base, DockerfileSpecific, LifecycleScripts, VSCodespecific } from './models/devContainer'; import { mkdir, writeFile } from 'fs/promises'; -import { Mission } from './mission'; -import { User, UserChallengeJson } from '../../model/user.model'; +import { Mission } from './models/mission'; import { GiteaPublicProperties } from '../../model/giteaPublicProperties.model'; import { commands, Uri } from 'vscode'; import { existsSync, copyFileSync } from 'fs'; import { join } from 'path'; import assert = require('assert'); +import UserChallenge, { User } from './models/userChallenge'; const remoteUserHomeDir = '/home/deadlock'; const remoteMissionDir = `${remoteUserHomeDir}/mission/`; @@ -30,7 +30,7 @@ export class MissionDevContainer { ) { let prefix = `${this.missionsWorkdir}`; if (this.isReviewingStudent()) { - prefix += `/students/${this.user.details.lastName}_${this.user.details.firstName}-${this.user.id}`; + prefix += `/students/${this.user.details.lastName}_${this.user.details.firstName}_${this.user.id}`; } this.dirs = { missionWorkdir: `${prefix}/${this.mission.id}`, @@ -66,14 +66,15 @@ export class MissionDevContainer { return writeFile( `${this.dirs.config}/user-challenge.json`, (() => { - const userChallengeJson: UserChallengeJson = { + const userChallengeJson: UserChallenge = { giteaHost: this.giteaProperties.sshHost, giteaSshPort: this.giteaProperties.sshPort, username: this.user.id.split('-').join(''), email: `${this.user.id.split('-').join('')}@deadlock.io`, missionId: this.mission.id, + missionVersion: this.mission.version, remoteGitUsername: this.user.id.split('-').join(''), - currentUserDetails: this.currentUser ? this.currentUser.details : this.user.details, + currentUserDetails: this.currentUser.details, remoteUserDetails: this.user.details, }; return JSON.stringify(userChallengeJson, null, 2); @@ -99,15 +100,14 @@ export class MissionDevContainer { private createDevContainerFile(options?: Partial<DockerfileSpecific & Base & VSCodespecific & LifecycleScripts>) { const hostMissionDevcontainerFileDir = `${this.dirs.devcontainer}/devcontainer.json`; const image = `${this.dockerImageUrl}/${this.mission.id}:${this.mission.version}`; - let extTarget; + let extTarget: string; this.mounts.push( `source=${userSshKeyFolderPath},target=/tmp/.ssh,type=bind,consistency=cached,readonly`, `source=${this.dirs.config},target=/home/config/,type=bind,consistency=cached,readonly`, 'source=/etc/hosts,target=/etc/hosts,type=bind,consistency=cached,readonly', ); if (process.env.DL_MOUNT_EXTENSION === 'true') { - const extPath = - '/home/takima/Desktop/d-projects/deadlock-desktop/deadlock-plugins/deadlock-extension/deadlock-coding-0.1.10.vsix'; + const extPath = process.env.EXTENSION_PATH; extTarget = '/injected-extension/extension.vsix'; this.mounts.push(`source=${extPath},target=${extTarget},type=bind`); } else { diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/devContainer.ts b/deadlock-plugins/deadlock-extension/src/core/mission/models/devContainer.ts similarity index 100% rename from deadlock-plugins/deadlock-extension/src/core/mission/devContainer.ts rename to deadlock-plugins/deadlock-extension/src/core/mission/models/devContainer.ts diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/mission.ts b/deadlock-plugins/deadlock-extension/src/core/mission/models/mission.ts similarity index 100% rename from deadlock-plugins/deadlock-extension/src/core/mission/mission.ts rename to deadlock-plugins/deadlock-extension/src/core/mission/models/mission.ts diff --git a/deadlock-plugins/deadlock-extension/src/model/user.model.ts b/deadlock-plugins/deadlock-extension/src/core/mission/models/userChallenge.ts similarity index 52% rename from deadlock-plugins/deadlock-extension/src/model/user.model.ts rename to deadlock-plugins/deadlock-extension/src/core/mission/models/userChallenge.ts index 7744bdc8f081d3c950926eb76932c3dc6078225b..46f75dd7403b2e67556fd5458ab656ab5b0069bf 100644 --- a/deadlock-plugins/deadlock-extension/src/model/user.model.ts +++ b/deadlock-plugins/deadlock-extension/src/core/mission/models/userChallenge.ts @@ -1,37 +1,30 @@ -import { GiteaPublicProperties } from './giteaPublicProperties.model'; - -export interface User { - id: string; - details: UserDetails; -} - -export interface UserDetails { - firstName: string; - lastName: string; - organization: string; - email: string; - login: string; - roles: string[]; - avatarUrl: string; -} - -export interface UserChallengeJson { +interface UserChallenge { giteaHost: string; giteaSshPort: number; username: string; email: string; missionId: string; + missionVersion: string; remoteGitUsername: string; currentUserDetails: UserDetails; remoteUserDetails: UserDetails; } -export interface UserChallengeJson2 { - gitea: Pick<GiteaPublicProperties, 'sshPort' | 'sshHost'>; - username: string; +export default UserChallenge; + +export interface UserDetails { + firstName: string; + lastName: string; + organization: string; email: string; - missionId: string; - remoteGitUsername: string; - currentUserDetails: UserDetails; - remoteUserDetails: UserDetails; + login: string; + roles: Set< + 'ADMIN' | 'MANAGER' | 'OFFLINE_ACCESS' | 'PROFESSOR' | 'UMA_AUTHORIZATION' | 'CANDIDATE' | 'DEFAULT-ROLES-DEADLOCK' + >; + avatarUrl: string; +} + +export interface User { + id: string; + details: UserDetails; } diff --git a/deadlock-plugins/deadlock-extension/src/core/userConfig.ts b/deadlock-plugins/deadlock-extension/src/core/userConfig.ts index 0f68f1fd05c455d27197d9aba1faba4cde7f699f..2d6928893d12283da2731d41db7a7a20d1a919aa 100644 --- a/deadlock-plugins/deadlock-extension/src/core/userConfig.ts +++ b/deadlock-plugins/deadlock-extension/src/core/userConfig.ts @@ -14,8 +14,8 @@ * "missionId":"code_persist_cdb_crud" * } */ -import { UserDetails } from '../model/user.model'; import { recorderError as error } from '../recorder/utils/log'; +import { UserDetails } from './mission/models/userChallenge'; export default abstract class UserConfig { private userConfigJson: any | undefined; diff --git a/deadlock-plugins/deadlock-extension/src/extension.ts b/deadlock-plugins/deadlock-extension/src/extension.ts index 3f52e208f5b7ffa487996eb8b25f324c79339991..6e86ff1ba0c025ba32b57bf4630f349669d1e43a 100644 --- a/deadlock-plugins/deadlock-extension/src/extension.ts +++ b/deadlock-plugins/deadlock-extension/src/extension.ts @@ -5,18 +5,21 @@ import { extensionError as error } from './recorder/utils/log'; import { DepNodeProvider } from './theia/deadlockPanel'; import UserConfigTheia from './theia/userConfigTheia'; import { CommandTreeProvider } from './view/CommandTree'; +import StartedMissionsView from './view/startedMissionsView'; export const userConfig = new UserConfigTheia(); export async function activate(context: ExtensionContext) { window.showInformationMessage('Bienvenue sur Deadlock!'); - new Controller(context); + Controller.getInstance(context); const workspaceFolders = workspace.workspaceFolders?.toString() ?? ''; if (!workspaceFolders) window.showInformationMessage('Pas de répertoires ouverts'); const deadlockPanelProvider = new DepNodeProvider(); window.registerTreeDataProvider('deadlockPanel', deadlockPanelProvider); + window.registerWebviewViewProvider('startedMissions', new StartedMissionsView(context)); + if (isDocker()) { commands.executeCommand('setContext', 'deadlock.inContainer', true); window.registerTreeDataProvider('commandTree', new CommandTreeProvider(context)); @@ -26,6 +29,7 @@ export async function activate(context: ExtensionContext) { try { await userConfig.init(); } catch (e) { + error(JSON.stringify(e)); error('Cannot init userConfig'); } } diff --git a/deadlock-plugins/deadlock-extension/src/recorder/utils/log.ts b/deadlock-plugins/deadlock-extension/src/recorder/utils/log.ts index aef8d4715ae945ee92bdf91c5edd381262372100..e231a4d4999f462ac688208ef92393196b95fc9f 100644 --- a/deadlock-plugins/deadlock-extension/src/recorder/utils/log.ts +++ b/deadlock-plugins/deadlock-extension/src/recorder/utils/log.ts @@ -6,6 +6,14 @@ export const log = (prefix: string, message: any, ...args: any[]) => { } }; +export const warn = (prefix: string, message: any, ...args: any[]) => { + if (args) { + console.warn(`[${prefix}] ${message}`, ...args); + } else { + console.warn(`[${prefix}] ${message}`); + } +}; + export const error = (prefix: string, message: any, ...args: any[]) => { if (args) { console.error(`[${prefix}] ${message}`, ...args); @@ -29,3 +37,7 @@ export function extensionError(message: any, ...args: any[]) { export function extensionLog(message: any, ...args: any[]) { log('DEADLOCK-EXTENSION', message, ...args); } + +export function extensionWarn(message: any, ...args: any[]) { + warn('DEADLOCK-EXTENSION', message, ...args); +} diff --git a/deadlock-plugins/deadlock-extension/src/view/CommandTree.ts b/deadlock-plugins/deadlock-extension/src/view/CommandTree.ts index a7e7c5b2a66382c5f0394ac6693310814ab4ad20..6503a2cdc5d601f64b14ac72939fa3912eca216d 100644 --- a/deadlock-plugins/deadlock-extension/src/view/CommandTree.ts +++ b/deadlock-plugins/deadlock-extension/src/view/CommandTree.ts @@ -3,7 +3,6 @@ import ChallengeYaml, { MissionCommand as MissionCommandYaml } from '../model/ch import { parse } from 'yaml'; import { readFileSync } from 'fs'; import { commands, ExtensionContext, ThemeColor, ThemeIcon, TreeDataProvider, TreeItem, window } from 'vscode'; -import { extensionLog } from '../recorder/utils/log'; export class CommandTreeProvider implements TreeDataProvider<MissionCommand> { challenge: ChallengeYaml; @@ -40,7 +39,6 @@ class MissionCommand extends TreeItem { this.iconPath = new ThemeIcon('notebook-execute', new ThemeColor('debugIcon.startForeground')); commands.registerCommand(missionCommandYaml.name, () => { const terminalName = `Deadlock`; - extensionLog(JSON.stringify(window.terminals)); const terminal = window.terminals.filter((terminal) => terminal.name === terminalName)[0] ?? window.createTerminal({ diff --git a/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts b/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts new file mode 100644 index 0000000000000000000000000000000000000000..463d15dfd12c2670a7164eff9bf4f127bf112451 --- /dev/null +++ b/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts @@ -0,0 +1,92 @@ +import { randomBytes } from 'crypto'; +import { existsSync, readdirSync, readFileSync } from 'fs'; +import { join } from 'path'; +import { ExtensionContext, Webview, WebviewView, WebviewViewProvider, window } from 'vscode'; +import Controller from '../core/controller'; +import ExtensionStore from '../core/extensionStore'; +import UserChallenge from '../core/mission/models/userChallenge'; +import { extensionWarn } from '../recorder/utils/log'; +import { getUri } from './webviewBase'; + +export default class StartedMissionsView implements WebviewViewProvider { + private readonly controller: Controller; + private readonly missionsWorkdir: string; + constructor(private context: ExtensionContext) { + this.missionsWorkdir = ExtensionStore.getInstance(this.context).getMissionWorkdir() ?? ''; + this.controller = Controller.getInstance(context); + } + + resolveWebviewView(webviewView: WebviewView): void | Thenable<void> { + webviewView.webview.options = { + enableScripts: true, + + localResourceRoots: [this.context.extensionUri], + }; + webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + webviewView.webview.onDidReceiveMessage(this.onMessageReceive.bind(this)); + } + + onMessageReceive(message: any): void { + switch (message.command) { + case 'openMission': { + window.showInformationMessage('click received: ' + message.mission); + const path = join(this.missionsWorkdir, message.mission, '.config', 'user-challenge.json'); + const userChallenge: UserChallenge = JSON.parse(readFileSync(path, 'utf8')); + this.controller.launchMission(userChallenge.missionId, userChallenge.missionVersion); + return; + } + } + } + + private _getHtmlForWebview(webview: Webview) { + const toolkitUri = getUri(webview, this.context.extensionUri, [ + 'node_modules', + '@vscode', + 'webview-ui-toolkit', + 'dist', + 'toolkit.js', + ]); + + const css = getUri(webview, this.context.extensionUri, ['resources', 'styles', 'startedMissionsView.css']); + + const js = getUri(webview, this.context.extensionUri, ['resources', 'js', 'startedMissionsView.js']); + + // find all folders in mission directory + const path = this.missionsWorkdir; + let missionsHtml = ''; + existsSync(path) && + readdirSync(path).forEach((mission) => { + if (existsSync(join(path, mission, '.config', 'user-challenge.json'))) { + try { + const userChallenge: UserChallenge = JSON.parse( + readFileSync(join(path, mission, '.config', 'user-challenge.json'), 'utf8'), + ); + missionsHtml += `<vscode-button onclick="openMission('${userChallenge.missionId}')" class="item" appearance="primary">${userChallenge.missionId}</vscode-button>`; + } catch (e) { + extensionWarn(`Failed to read user-challenge.json for mission ${mission}`); + } + } else { + extensionWarn(`Folder '${mission}' has no user-challenge.json file so it is ignored.`); + } + }); + + if (!missionsHtml) { + missionsHtml = '<div class="item">Aucune mission n\'a été démarrée</div>'; + } + + return `<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <script type="module" src="${toolkitUri}"></script> + <script nonce="${randomBytes(16).toString('base64')}" src="${js}"></script> + <link rel="stylesheet" type="text/css" href="${css}"> + </head> + <body> + <div> + ${missionsHtml} + </div> + </body> + </html>`; + } +}