diff --git a/package.json b/package.json index 08dc11528..6e939015b 100644 --- a/package.json +++ b/package.json @@ -838,6 +838,20 @@ "light": "images/title/light/icon-refresh.svg" } }, + { + "command": "openshift.componentTypesView.registry.add", + "title": "Add Registry", + "category": "OpenShift", + "icon": { + "dark": "images/title/dark/add-cluster.svg", + "light": "images/title/light/add-cluster.svg" + } + }, + { + "command": "openshift.componentTypesView.registry.remove", + "title": "Remove", + "category": "OpenSift" + }, { "command": "openshift.welcome", "title": "Welcome", @@ -1111,9 +1125,22 @@ { "command": "openshift.component.revealContextInExplorer", "when": "false" + }, + { + "command": "openshift.componentTypesView.registry.add", + "when": "false" + }, + { + "command": "openshift.componentTypesView.registry.remove", + "when": "false" } ], "view/title": [ + { + "command": "openshift.componentTypesView.registry.add", + "when": "view == openshiftComponentTypesView", + "group": "navigation" + }, { "command": "openshift.componentTypesView.refresh", "when": "view == openshiftComponentTypesView", @@ -1495,6 +1522,10 @@ "when": "view == openshiftComponentTypesView && viewItem == s2iImageStreamTag || viewItem == devfileStarterProject || viewItem == devfileComponentType", "group": "0@0" }, + { + "command": "openshift.componentTypesView.registry.remove", + "when": "view == openshiftComponentTypesView && viewItem == devfileRegistry" + }, { "command": "openshift.component.revealInExplorer", "when": "view == openshiftComponentsView && viewItem == openshift.component" diff --git a/src/componentTypesView.ts b/src/componentTypesView.ts index 83765df06..a07477a23 100644 --- a/src/componentTypesView.ts +++ b/src/componentTypesView.ts @@ -18,7 +18,8 @@ import * as path from 'path'; import { CliExitData } from './cli'; import { getInstance, - Odo + Odo, + OdoImpl } from './odo'; import { Command } from './odo/command'; import { @@ -44,6 +45,7 @@ import { vsCommand, VsCommandError } from './vscommand'; import { Cluster } from '@kubernetes/client-node/dist/config_types'; import { KubeConfig } from '@kubernetes/client-node'; import { Platform } from './util/platform'; +import * as validator from 'validator'; type ExampleProject = SampleProject | StarterProject; type ComponentType = DevfileComponentType | ImageStreamTag | ExampleProject | Cluster | Registry; @@ -104,6 +106,7 @@ export class ComponentTypesView implements TreeDataProvider { if (isRegistry(element)) { return { label: element.Name, + contextValue: ContextType.DEVFILE_REGISTRY, tooltip: `Devfile Registry\nName: ${element.Name}\nURL: ${element.URL}`, collapsibleState: TreeItemCollapsibleState.Collapsed, } @@ -182,6 +185,21 @@ export class ComponentTypesView implements TreeDataProvider { return data; } + addRegistry(newRegistry: Registry): void { + this.registries.push(newRegistry); + this.refresh(false); + this.reveal(newRegistry); + + } + + removeRegistry(targetRegistry: Registry): void { + this.registries.splice( + this.registries.findIndex((registry) => registry.Name === targetRegistry.Name), + 1 + ); + this.refresh(false); + } + private async getRegistries(): Promise { if(!this.registries) { this.registries = await this.odo.getRegistries(); @@ -239,8 +257,14 @@ export class ComponentTypesView implements TreeDataProvider { return undefined; } - refresh(): void { - this.registries = undefined; + reveal(item: Registry): void { + this.treeView.reveal(item); + } + + refresh(cleanCache = true): void { + if (cleanCache) { + this.registries = undefined; + } this.onDidChangeTreeDataEmitter.fire(); } @@ -289,4 +313,69 @@ export class ComponentTypesView implements TreeDataProvider { return 'Cannot find sample project repository url'; } } + + @vsCommand('openshift.componentTypesView.registry.add') + public static async addRegistryCmd(): Promise { + // ask for registry + const regName = await window.showInputBox({ + prompt: 'Provide registry name to display in the view', + placeHolder: 'Registry Name', + validateInput: async (value) => { + const trimmedValue = value.trim(); + if (trimmedValue.length === 0) { + return 'Registry name cannot be empty' + } + if (!validator.matches(trimmedValue, '^[a-zA-Z0-9]+$')) { + return 'Registry name can have only alphabet characters and numbers'; + } + const registries = await ComponentTypesView.instance.getRegistries(); + if(registries.find((registry) => registry.Name === value)) { + return `Registry name '${value}' is already used`; + } + } + }); + + if (!regName) return null; + + const regURL = await window.showInputBox({ignoreFocusOut: true, + prompt: 'Provide registry URL to display in the view', + placeHolder: 'Registry URL', + validateInput: async (value) => { + const trimmedValue = value.trim(); + if (!validator.isURL(trimmedValue)) { + return 'Entered URL is invalid' + } + const registries = await ComponentTypesView.instance.getRegistries(); + if(registries.find((registry) => registry.URL === value)) { + return `Registry with entered URL '${value}' already exists`; + } + } + }); + + if (!regURL) return null; + + const secure = await window.showQuickPick(['Yes', 'No'], { + placeHolder: 'Is it a secure registry?' + }); + + if (!secure) return null; + + let token: string; + if (secure === 'Yes') { + token = await window.showInputBox({placeHolder: 'Token to access the registry'}); + if (!token) return null; + } + + const newRegistry = await OdoImpl.Instance.addRegistry(regName, regURL, token); + ComponentTypesView.instance.addRegistry(newRegistry); + } + + @vsCommand('openshift.componentTypesView.registry.remove') + public static async removeRegistry(registry: Registry): Promise { + const yesNo = await window.showInformationMessage(`Remove registry '${registry.Name}'?`, 'Yes', 'No'); + if (yesNo === 'Yes') { + await OdoImpl.Instance.removeRegistry(registry.Name); + ComponentTypesView.instance.removeRegistry(registry); + } + } } diff --git a/src/odo.ts b/src/odo.ts index 26bae47a6..c5a8bea13 100644 --- a/src/odo.ts +++ b/src/odo.ts @@ -364,6 +364,8 @@ export interface Odo { loadItems(result: cliInstance.CliExitData, fetch: (data) => I[]): I[]; getRegistries(): Promise; readonly subject: Subject; + addRegistry(name: string, url: string, token: string): Promise; + removeRegistry(name: string): Promise; } class OdoModel { @@ -1113,6 +1115,18 @@ export class OdoImpl implements Odo { return this.loadItemsFrom(result, (data) => data.registries); } + public async addRegistry(name: string, url: string, token: string): Promise { + await this.execute(Command.addRegistry(name, url, token)); + return { + Name: name, + Secure: true, + URL: url + }; + } + + public async removeRegistry(name: string): Promise { + await this.execute(Command.removeRegistry(name)); + } } export function getInstance(): Odo { diff --git a/src/odo/command.ts b/src/odo/command.ts index a116a4367..604761a36 100644 --- a/src/odo/command.ts +++ b/src/odo/command.ts @@ -141,6 +141,18 @@ export class Command { return new CommandText('odo registry list -o json'); } + static addRegistry(name: string, url: string, token: string): CommandText { + const cTxt = new CommandText('odo registry add', `${name} ${url}`); + if (token) { + cTxt.addOption(new CommandOption('--token', token)); + } + return cTxt; + } + + static removeRegistry(name: string): CommandText { + return new CommandText('odo registry delete', name, [new CommandOption('-f')]); + } + static listCatalogComponents(): CommandText { return new CommandText('odo catalog list components'); }