Skip to content

Commit 5ed1e8c

Browse files
authored
Components View first iteration (#2012)
* Components View first iteration This PR fixes #2008. This fix adds: 1. Components View (View) under Application Explorer 2. View shows components discovered in workspace root folders 3. Detection for devfile components based on odo env view, which works when cluster is not accessible or user is logged off from cluster 4. Detection for s2i components based on parsing .odo/config.yaml (forkaround for odo env view not working for s2i components) 5. Refresh for View based after event about new folder added to workspace 6. Refrehs for View after component created form existing workspace folder (to avoid using watch for every folder in workspace) 7. Reveal in Explorer for components to activate Explorer view and select context folder (not always working correctly first time, have no idea why) 8. Support to create component when there is no accessible cluster or user is logged off (there should be quick input requests for project and application names, there should be qiuickpiks provided that allow to select existing and not existing projects/applications) Not implemented yet: 1. Show folders/files under component node (requires some work to incorporate filesystem watching with exlusion for folders like 'node_modules' 2. Watching for filesystem changes or focus ganed event to detect changes done in vscode terminal or external terminal 3. Showing hierarchy Project/Application/Component rather than flat list 4. Support pushing components to cluster from the view, especially for components with project that does not exist or currently not active 5. Showing state Pushed/Not pushed for components pushed to current cluster/namespace (or current context) 6. No icons yet 7. No view Welcome page with 'Create Component' button Signed-off-by: Denis Golovin dgolovin@redhat.com
1 parent ba60970 commit 5ed1e8c

27 files changed

+411
-39
lines changed

package.json

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,20 @@
791791
"command": "openshift.component.test.palette",
792792
"title": "Test Component",
793793
"category": "OpenShift"
794+
},
795+
{
796+
"command": "openshift.component.revealInExplorer",
797+
"title": "Reveal in Explorer",
798+
"category": "OpenShift"
799+
},
800+
{
801+
"command": "openshift.componentsView.refresh",
802+
"title": "Refresh Components View",
803+
"category": "OpenShift",
804+
"icon": {
805+
"dark": "images/title/dark/icon-refresh.svg",
806+
"light": "images/title/light/icon-refresh.svg"
807+
}
794808
}
795809

796810
],
@@ -826,6 +840,10 @@
826840
"id": "openshiftProjectExplorer",
827841
"name": "Application Explorer"
828842
},
843+
{
844+
"id": "openshiftComponentsView",
845+
"name": "Components"
846+
},
829847
{
830848
"id": "openshiftComponentTypesView",
831849
"name": "Component Types"
@@ -997,6 +1015,14 @@
9971015
{
9981016
"command": "openshift.explorer.addCluster.openCrcAddClusterPage",
9991017
"when": "false"
1018+
},
1019+
{
1020+
"command": "openshift.component.revealInExplorer",
1021+
"when": "false"
1022+
},
1023+
{
1024+
"command": "openshift.componentType.newComponent",
1025+
"when": "false"
10001026
}
10011027
],
10021028
"view/title": [
@@ -1005,6 +1031,11 @@
10051031
"when": "view == openshiftComponentTypesView",
10061032
"group": "navigation"
10071033
},
1034+
{
1035+
"command": "openshift.componentsView.refresh",
1036+
"when": "view == openshiftComponentsView",
1037+
"group": "navigation"
1038+
},
10081039
{
10091040
"command": "openshift.explorer.addCluster",
10101041
"when": "view == openshiftProjectExplorer && isVSCode == 1",
@@ -1365,6 +1396,10 @@
13651396
"command": "openshift.componentType.newComponent",
13661397
"when": "view == openshiftComponentTypesView && viewItem == s2iImageStreamTag || viewItem == devfileStarterProject || viewItem == devfileComponentType",
13671398
"group": "0@0"
1399+
},
1400+
{
1401+
"command": "openshift.component.revealInExplorer",
1402+
"when": "view == openshiftComponentsView && viewItem == openshift.component"
13681403
}
13691404
]
13701405
},
@@ -1424,4 +1459,4 @@
14241459
}
14251460
},
14261461
"extensionDependencies": []
1427-
}
1462+
}

src/base/baseTreeDataProvider.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*-----------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE file in the project root for license information.
4+
*-----------------------------------------------------------------------------------------------*/
5+
6+
import * as vsc from 'vscode';
7+
8+
export abstract class BaseTreeDataProvider<T> implements vsc.TreeDataProvider<T> {
9+
10+
protected treeView: vsc.TreeView<T>;
11+
12+
protected onDidChangeTreeDataEmitter: vsc.EventEmitter<T> =
13+
new vsc.EventEmitter<T | undefined>();
14+
15+
readonly onDidChangeTreeData: vsc.Event<T | undefined> = this
16+
.onDidChangeTreeDataEmitter.event;
17+
18+
public createTreeView(id: string): vsc.TreeView<T> {
19+
if (!this.treeView) {
20+
this.treeView = vsc.window.createTreeView(id, {
21+
treeDataProvider: this,
22+
});
23+
}
24+
return this.treeView;
25+
}
26+
27+
abstract getTreeItem(element: T): vsc.TreeItem | Thenable<vsc.TreeItem>;
28+
abstract getChildren(element?: T): vsc.ProviderResult<T[]>;
29+
}

src/componentsView.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*-----------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE file in the project root for license information.
4+
*-----------------------------------------------------------------------------------------------*/
5+
6+
import { pathExistsSync, readFileSync } from 'fs-extra';
7+
import * as path from 'path';
8+
import * as vsc from 'vscode';
9+
import * as jsYaml from 'js-yaml';
10+
import { BaseTreeDataProvider } from './base/baseTreeDataProvider';
11+
import { CliExitData } from './cli';
12+
import { getInstance } from './odo';
13+
import { Command } from './odo/command';
14+
import { EnvInfo } from './odo/env';
15+
import { vsCommand } from './vscommand';
16+
17+
18+
export interface WorkspaceEntry {
19+
uri: vsc.Uri;
20+
type: vsc.FileType;
21+
}
22+
23+
function isWorkspaceEntry(entry: any): entry is WorkspaceEntry {
24+
return entry.uri && entry.type;
25+
}
26+
27+
class Labeled {
28+
constructor(public label: string) {}
29+
}
30+
31+
class Project extends Labeled {
32+
}
33+
34+
class Application extends Labeled {
35+
}
36+
37+
type Entry = Project | Application | WorkspaceEntry | WorkspaceFolderComponent;
38+
39+
export interface WorkspaceFolderComponent {
40+
project: string;
41+
application: string;
42+
contextUri: vsc.Uri;
43+
label: string;
44+
description?: string;
45+
tooltip?: string;
46+
}
47+
48+
function isWorkspaceFolderComponent(entry: any): entry is WorkspaceFolderComponent {
49+
return entry.contextUri;
50+
}
51+
52+
async function getComponentsInWorkspace(): Promise<WorkspaceFolderComponent[]> {
53+
const execs: Promise<CliExitData>[] = [];
54+
vsc.workspace.workspaceFolders.forEach((folder)=> {
55+
try {
56+
execs.push(getInstance().execute(Command.viewEnv(), folder.uri.fsPath, false));
57+
} catch (ignore) {
58+
// ignore execution errors
59+
}
60+
});
61+
const results = await Promise.all(execs);
62+
return results.map((result) => {
63+
try {
64+
let compData: EnvInfo;
65+
if (result.error) {
66+
// detect S2I component manually
67+
const pathToS2iConfig = path.join(result.cwd, '.odo', 'config.yaml');
68+
if (pathExistsSync(pathToS2iConfig)) {
69+
// reconstruct env view form yaml file data
70+
const s2iConf = jsYaml.load(readFileSync(pathToS2iConfig, 'utf8'));
71+
compData = {
72+
spec: {
73+
appName: s2iConf.ComponentSettings.Application,
74+
project: s2iConf.ComponentSettings.Project,
75+
name: s2iConf.ComponentSettings.Name,
76+
}
77+
}
78+
}
79+
} else {
80+
compData = JSON.parse(result.stdout) as EnvInfo;
81+
}
82+
const contextUri = vsc.Uri.parse(result.cwd);
83+
const project = compData.spec.project ? compData.spec.project : 'N/A';
84+
const application = compData.spec.appName ? compData.spec.appName : 'N/A';
85+
const tooltip = ['Component',
86+
`Name: ${compData.spec.name}`,
87+
`Project: ${project}`,
88+
`Application: ${application}`,
89+
`Context: ${contextUri.fsPath}`,
90+
].join('\n');
91+
const description = `${project}/${application}`;
92+
return {
93+
project,
94+
application,
95+
label: compData.spec.name,
96+
contextUri,
97+
description,
98+
tooltip,
99+
contextValue: 'openshift.component'
100+
}
101+
} catch (err) {
102+
// ignore unexpected parsing errors
103+
}
104+
});
105+
}
106+
107+
export class ComponentsTreeDataProvider extends BaseTreeDataProvider<Entry> {
108+
109+
static dataProviderInstance: ComponentsTreeDataProvider;
110+
111+
private constructor() {
112+
super();
113+
vsc.workspace.onDidChangeWorkspaceFolders(() => {
114+
this.refresh();
115+
});
116+
}
117+
118+
private refresh(): void {
119+
this.onDidChangeTreeDataEmitter.fire();
120+
}
121+
122+
@vsCommand('openshift.componentsView.refresh')
123+
public static refresh(): void {
124+
ComponentsTreeDataProvider.instance.refresh();
125+
}
126+
127+
@vsCommand('openshift.component.revealInExplorer')
128+
public static async revealInExplorer(context: Entry): Promise<void> {
129+
if (isWorkspaceFolderComponent(context)) {
130+
await vsc.commands.executeCommand('workbench.view.explorer', );
131+
await vsc.commands.executeCommand('revealInExplorer', context.contextUri);
132+
}
133+
}
134+
createTreeView(id: string): vsc.TreeView<Entry> {
135+
if (!this.treeView) {
136+
this.treeView = vsc.window.createTreeView(id, {
137+
treeDataProvider: this,
138+
});
139+
}
140+
return this.treeView;
141+
}
142+
143+
static get instance(): ComponentsTreeDataProvider {
144+
if (!ComponentsTreeDataProvider.dataProviderInstance) {
145+
ComponentsTreeDataProvider.dataProviderInstance = new ComponentsTreeDataProvider();
146+
}
147+
return ComponentsTreeDataProvider.dataProviderInstance;
148+
}
149+
150+
getTreeItem(element: Entry): vsc.TreeItem {
151+
if (isWorkspaceFolderComponent(element)) {
152+
return element;
153+
}
154+
}
155+
156+
getChildren(element?: Entry): vsc.ProviderResult<Entry[]> {
157+
if (element) {
158+
if (isWorkspaceFolderComponent(element)) {
159+
return [];
160+
} else if (isWorkspaceEntry(element)) {
161+
return []
162+
}
163+
}
164+
return getComponentsInWorkspace();
165+
}
166+
}

src/extension.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ import { ToolsConfig } from './tools';
2626
import { extendClusterExplorer } from './k8s/clusterExplorer';
2727
import { WatchSessionsView } from './watch';
2828
import { DebugSessionsView } from './debug';
29-
import { ComponentTypesView } from './component';
29+
import { ComponentTypesView } from './componentTypesView';
3030

3131
import fsx = require('fs-extra');
32+
import { ComponentsTreeDataProvider } from './componentsView';
3233

3334
// eslint-disable-next-line @typescript-eslint/no-empty-function
3435
// this method is called when your extension is deactivated
@@ -72,7 +73,8 @@ export async function activate(extensionContext: ExtensionContext): Promise<any>
7273
'./openshift/service',
7374
'./k8s/console',
7475
'./oc',
75-
'./component',
76+
'./componentTypesView',
77+
'./componentsView'
7678
)),
7779
commands.registerCommand('clusters.openshift.useProject', (context) =>
7880
commands.executeCommand('extension.vsKubernetesUseNamespace', context),
@@ -83,6 +85,7 @@ export async function activate(extensionContext: ExtensionContext): Promise<any>
8385
new DebugSessionsView().createTreeView('openshiftDebugView'),
8486
...Component.init(extensionContext),
8587
ComponentTypesView.instance.createTreeView('openshiftComponentTypesView'),
88+
ComponentsTreeDataProvider.instance.createTreeView('openshiftComponentsView'),
8689
];
8790
disposable.forEach((value) => extensionContext.subscriptions.push(value));
8891

src/oc.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
import { window } from 'vscode';
77
import { CliChannel } from './cli';
88
import { ToolsConfig } from './tools';
9-
import OpenShiftItem from './openshift/openshiftItem';
9+
import OpenShiftItem, { clusterRequired } from './openshift/openshiftItem';
1010
import { vsCommand } from './vscommand';
1111

1212
export class Oc {
1313

1414
@vsCommand('openshift.create')
15+
@clusterRequired()
1516
public static async create(): Promise<string | null> {
1617
const document = window.activeTextEditor ? window.activeTextEditor.document : undefined;
1718
const pleaseSave = 'Please save your changes before executing \'OpenShift: Create\' command.';

src/odo.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,12 +863,15 @@ export class OdoImpl implements Odo {
863863

864864
public async createComponentFromFolder(application: OpenShiftObject, type: string, version: string, name: string, location: Uri, starter: string = undefined, useExistingDevfile = false): Promise<OpenShiftObject> {
865865
await this.execute(Command.createLocalComponent(application.getParent().getName(), application.getName(), type, version, name, location.fsPath, starter, useExistingDevfile), location.fsPath);
866-
if (workspace.workspaceFolders) {
866+
if (workspace.workspaceFolders && application.getParent().getParent()) { // if there are workspace folders and cluster is acvessible
867867
const targetApplication = (await this.getApplications(application.getParent())).find((value) => value === application);
868868
if (!targetApplication) {
869869
await this.insertAndReveal(application);
870870
}
871871
await this.insertAndReveal(new OpenShiftComponent(application, name, ContextType.COMPONENT, location, 'local', version ? ComponentKind.S2I : ComponentKind.DEVFILE, {name: type? type : name , tag: version}));
872+
} else {
873+
OdoImpl.data.delete(application);
874+
OdoImpl.data.delete(application.getParent());
872875
}
873876
let wsFolder: WorkspaceFolder;
874877
if (workspace.workspaceFolders) {

src/odo/command.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ function verbose(_target: any, key: string, descriptor: any): void {
2525
}
2626

2727
export class Command {
28+
29+
static viewEnv(): string {
30+
return 'odo env view -o json';
31+
}
32+
2833
static printCatalogComponentImageStreamRefJson(name: string, namespace: string): string {
2934
return `oc get imagestream ${name} -n ${namespace} -o json`;
3035
}

src/odo/env.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*-----------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE file in the project root for license information.
4+
*-----------------------------------------------------------------------------------------------*/
5+
6+
export interface EnvInfo {
7+
kind?: 'EnvInfo';
8+
apiVersion?: 'odo.dev/v1alpha1';
9+
metadata?: {
10+
creationTimestamp: string;
11+
},
12+
spec: {
13+
name: string;
14+
project: string;
15+
appName: string;
16+
}
17+
}

src/openshift/application.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*-----------------------------------------------------------------------------------------------*/
55

66
import { window } from 'vscode';
7-
import OpenShiftItem from './openshiftItem';
7+
import OpenShiftItem, { clusterRequired } from './openshiftItem';
88
import { OpenShiftObject } from '../odo';
99
import { Command } from '../odo/command';
1010
import { Progress } from '../util/progress';
@@ -13,13 +13,15 @@ import { vsCommand, VsCommandError } from '../vscommand';
1313
export class Application extends OpenShiftItem {
1414

1515
@vsCommand('openshift.app.describe', true)
16+
@clusterRequired()
1617
static async describe(treeItem: OpenShiftObject): Promise<void> {
1718
const application = await Application.getOpenShiftCmdData(treeItem,
1819
'Select Application you want to describe');
1920
if (application) Application.odo.executeInTerminal(Command.describeApplication(application.getParent().getName(), application.getName()), undefined, `OpenShift: Describe '${application.getName()}' Application`);
2021
}
2122

2223
@vsCommand('openshift.app.delete', true)
24+
@clusterRequired()
2325
static async del(treeItem: OpenShiftObject): Promise<string> {
2426
const application = await Application.getOpenShiftCmdData(treeItem,
2527
'Select Application to delete');

0 commit comments

Comments
 (0)