Skip to content
This repository has been archived by the owner on Apr 4, 2023. It is now read-only.

[WIP] Synchronize the changes between the configuration files and workspace devfile #1095

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
9 changes: 8 additions & 1 deletion extensions/eclipse-che-theia-workspace/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@eclipse-che/api": "latest",
"@theia/workspace": "next",
"@eclipse-che/theia-remote-api": "^0.0.1",
"@eclipse-che/theia-plugin-ext": "^0.0.1",
"js-yaml": "3.13.1"
},
"devDependencies": {
Expand Down Expand Up @@ -47,6 +48,12 @@
"modulePathIgnorePatterns": [
"<rootDir>/lib"
],
"preset": "ts-jest"
"preset": "ts-jest",
"moduleNameMapper": {
"\\.(css|less)$": "<rootDir>/tests/mock.js"
},
"setupFilesAfterEnv": [
"<rootDir>/tests/browser/frontend-application-config-provider.ts"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import '../../src/browser/style/index.css';

import { CommandContribution, MenuContribution } from '@theia/core/lib/common';
import { Container, ContainerModule, interfaces } from 'inversify';
import {
DevfileWatcher,
ExtensionsJsonWatcher,
PluginsYamlWatcher,
TasksJsonWatcher,
} from './workspace-config-files-watcher';
import { FileTree, FileTreeModel, FileTreeWidget, createFileTreeContainer } from '@theia/filesystem/lib/browser';
import {
FrontendApplicationContribution,
Expand Down Expand Up @@ -48,6 +54,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).to(ExplorerContribution);

rebind(FileNavigatorWidget).toDynamicValue(ctx => createFileNavigatorWidget(ctx.container));

// const devWorkspaceName = process.env['DEVWORKSPACE_NAME'];
// if (devWorkspaceName) {
bind(DevfileWatcher).toSelf().inSingletonScope();
bind(ExtensionsJsonWatcher).toSelf().inSingletonScope();
bind(PluginsYamlWatcher).toSelf().inSingletonScope();
bind(TasksJsonWatcher).toSelf().inSingletonScope();
[DevfileWatcher, ExtensionsJsonWatcher, PluginsYamlWatcher, TasksJsonWatcher].forEach(component => {
bind(FrontendApplicationContribution).to(component);
});
// }
});

export function createFileNavigatorContainer(parent: interfaces.Container): Container {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**********************************************************************
* Copyright (c) 2021 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 * as jsYaml from 'js-yaml';

import { FileChangeType, FileChangesEvent } from '@theia/filesystem/lib/common/files';
import { FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable } from 'inversify';

import { ChePluginManager } from '@eclipse-che/theia-plugin-ext/lib/browser/plugin/che-plugin-manager';
import { DevfileService } from '@eclipse-che/theia-remote-api/lib/common/devfile-service';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';

import debounce = require('lodash.debounce');

/**
* Abstract watcher allows to track the changes in the project-specific configuration files.
* A concrete implementation can handle the changes in a specific way.
*/
@injectable()
export abstract class AbstractFileWatcher implements FrontendApplicationContribution {
@inject(FileService)
protected readonly fileService: FileService;

@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;

/** File name to watch, e.g. '.vscode/extensions.json'. */
protected abstract fileName: string;

/**
* Called when the frontend application is started.
*/
async onStart(app: FrontendApplication): Promise<void> {
this.trackFilesInRoots();
this.workspaceService.onWorkspaceChanged(() => this.trackFilesInRoots());
}

private async trackFilesInRoots(): Promise<void> {
(await this.workspaceService.roots).forEach(root => {
const fileURI = root.resource.resolve(this.fileName);
this.fileService.watch(fileURI);
const onFileChange = async (event: FileChangesEvent) => {
if (event.contains(fileURI, FileChangeType.ADDED)) {
this.handleChange(fileURI, FileChangeType.ADDED);
} else if (event.contains(fileURI, FileChangeType.UPDATED)) {
this.handleChange(fileURI, FileChangeType.UPDATED);
}
};
this.fileService.onDidFilesChange(debounce(onFileChange, 1000));
});
}

/**
* Allows an implementor to handle a file change.
*
* @param fileURI an URI of the modified file
* @param changeType file change type
*/
protected abstract handleChange(fileURI: URI, changeType: FileChangeType): void;
}

@injectable()
export class DevfileWatcher extends AbstractFileWatcher {
protected fileName = 'devfile.yaml';

@inject(DevfileService)
protected readonly devfileService: DevfileService;

@inject(ChePluginManager)
protected readonly chePluginManager: ChePluginManager;

@inject(MessageService)
protected readonly messageService: MessageService;

protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise<void> {
const message =
changeType === FileChangeType.ADDED
? `A Devfile is found in ${fileURI}. Do you want to update your Workspace?`
: 'Do you want to update your Workspace with the changed Devfile?';
const answer = await this.messageService.info(message, 'Yes', 'No');
if (answer === 'Yes') {
this.updateWorkspaceWithDevfile(fileURI);
}
}

/**
* Updates the workspace with the given Devfile.
*
* @param devfileURI URI of the Devfile to update the Workspace with
*/
protected async updateWorkspaceWithDevfile(devfileURI: URI): Promise<void> {
const content = await this.fileService.readFile(devfileURI);
const devfile = jsYaml.load(content.value.toString());
await this.devfileService.updateDevfile(devfile);
await this.chePluginManager.restartWorkspace();
}
}

@injectable()
export class ExtensionsJsonWatcher extends AbstractFileWatcher {
protected fileName = '.vscode/extensions.json';

@inject(ChePluginManager)
protected readonly chePluginManager: ChePluginManager;

@inject(MessageService)
protected readonly messageService: MessageService;

protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise<void> {
const message =
changeType === FileChangeType.ADDED
? `An extensions list is found in ${fileURI}. Do you want to update your Workspace with these extensions?`
: 'Do you want to update your Workspace with the changed "extensions.json"?';
const answer = await this.messageService.info(message, 'Yes', 'No');
if (answer === 'Yes') {
await this.chePluginManager.restartWorkspace();
}
}
}

@injectable()
export class PluginsYamlWatcher extends AbstractFileWatcher {
protected fileName = '.che/che-theia-plugins.yaml';

@inject(ChePluginManager)
protected readonly chePluginManager: ChePluginManager;

@inject(MessageService)
protected readonly messageService: MessageService;

protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise<void> {
const message =
changeType === FileChangeType.ADDED
? `A plug-ins list is found in ${fileURI}. Do you want to update your Workspace with these plug-ins?`
: 'Do you want to update your Workspace with the changed "che-theia-plugins.yaml"?';
const answer = await this.messageService.info(message, 'Yes', 'No');
if (answer === 'Yes') {
await this.chePluginManager.restartWorkspace();
}
}
}

@injectable()
export class TasksJsonWatcher extends AbstractFileWatcher {
protected fileName = '.vscode/tasks.json';

@inject(MessageService)
protected readonly messageService: MessageService;

protected async handleChange(fileURI: URI, changeType: FileChangeType): Promise<void> {
const answer = await this.messageService.info(
'Do you want to update your Workspace with the "tasks.json" changes?',
'Yes',
'No'
);
if (answer === 'Yes') {
// TODO: set the tasks to the project's attributes
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/

describe('no-op', function () {
it('no-op', function () {});
import 'reflect-metadata';

import { ApplicationProps } from '@theia/application-package/lib/application-props';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';

FrontendApplicationConfigProvider.set({
...ApplicationProps.DEFAULT.frontend.config,
applicationName: 'test',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**********************************************************************
* Copyright (c) 2021 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 'reflect-metadata';

import { AbstractFileWatcher, DevfileWatcher } from '../../src/browser/workspace-config-files-watcher';

import { ChePluginManager } from '@eclipse-che/theia-plugin-ext/lib/browser/plugin/che-plugin-manager';
import { Container } from '@theia/core/shared/inversify';
import { DevfileService } from '@eclipse-che/theia-remote-api/lib/common/devfile-service';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { FrontendApplication } from '@theia/core/lib/browser';
import { MessageService } from '@theia/core';
import URI from '@theia/core/lib/common/uri';
import { WorkspaceService } from '@theia/workspace/lib/browser';

describe('Test workspace config files watchers', function () {
let container: Container;

let fileService: FileService;
let workspaceService: WorkspaceService;

const fileServiceWatchMethod = jest.fn();
const workspaceServiceOnWorkspaceChangedMethod = jest.fn();

beforeEach(() => {
jest.restoreAllMocks();
jest.resetAllMocks();

container = new Container();

fileService = ({
watch: fileServiceWatchMethod,
} as unknown) as FileService;

workspaceService = ({
onWorkspaceChanged: workspaceServiceOnWorkspaceChangedMethod,
} as unknown) as WorkspaceService;

container.bind(FileService).toConstantValue(fileService);
container.bind(WorkspaceService).toConstantValue(workspaceService);
});

describe('Test DevfileWatcher', function () {
let devfileWatcher: AbstractFileWatcher;

const devfileServiceUpdateDevfileMethod = jest.fn();
const chePluginManagerRestartWorkspaceMethod = jest.fn();
const messageServiceInfoMethod = jest.fn();

beforeEach(() => {
const devfileService = ({
updateDevfile: devfileServiceUpdateDevfileMethod,
} as unknown) as DevfileService;

const chePluginManager = ({
restartWorkspace: chePluginManagerRestartWorkspaceMethod,
} as unknown) as ChePluginManager;

const messageService = ({
info: messageServiceInfoMethod,
} as unknown) as MessageService;

container.bind(DevfileService).toConstantValue(devfileService);
container.bind(ChePluginManager).toConstantValue(chePluginManager);
container.bind(MessageService).toConstantValue(messageService);
container.bind(DevfileWatcher).toSelf().inSingletonScope();
devfileWatcher = container.get(DevfileWatcher);
});

test('shouldWatch', async () => {
const resolveFn = jest.fn();
const resource = ({
resolve: resolveFn,
} as unknown) as URI;
resolveFn.mockReturnValue(resource);

const roots: FileStat[] = [
{
name: 'testFile',
isDirectory: false,
isFile: true,
isSymbolicLink: false,
resource: resource,
},
];

Object.defineProperty(workspaceService, 'roots', {
get: jest.fn(() => roots),
});

await devfileWatcher.onStart({} as FrontendApplication);

expect(fileServiceWatchMethod.mock.calls.length).toEqual(1);
expect(fileServiceWatchMethod).toHaveBeenCalledWith(resource);
});
});
});
19 changes: 19 additions & 0 deletions extensions/eclipse-che-theia-workspace/tests/mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/********************************************************************************
* Copyright (C) 2021 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

'use strict';

module.exports = {};