diff --git a/build/esbuild.mjs b/build/esbuild.mjs index 50d71ddea..73cc35ee6 100644 --- a/build/esbuild.mjs +++ b/build/esbuild.mjs @@ -11,6 +11,7 @@ import * as fs from 'fs/promises'; const webviews = [ 'cluster', 'create-service', + 'create-route', 'create-component', 'devfile-registry', 'helm-chart', diff --git a/package.json b/package.json index cd94b351a..9db826bf4 100644 --- a/package.json +++ b/package.json @@ -807,6 +807,11 @@ "title": "Create Operator-Backed Service", "category": "OpenShift" }, + { + "command": "openshift.route.create", + "title": "Create Route", + "category": "OpenShift" + }, { "command": "openshift.deployment.create.fromImageUrl", "title": "Create Deployment from Container Image URL", @@ -1626,6 +1631,11 @@ "when": "view == openshiftProjectExplorer && isLoggedIn && viewItem =~ /openshift.project.*/i && showCreateService", "group": "c2" }, + { + "command": "openshift.route.create", + "when": "view == openshiftProjectExplorer && isLoggedIn && viewItem =~ /openshift.project.*/i && isOpenshiftCluster && showCreateRoute", + "group": "c2" + }, { "command": "openshift.deployment.create.fromImageUrl", "when": "view == openshiftProjectExplorer && isLoggedIn && viewItem =~ /openshift.project.*/i", diff --git a/src/explorer.ts b/src/explorer.ts index df3e8b52d..2fcc36fe7 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -27,7 +27,7 @@ import * as Helm from './helm/helm'; import { HelmRepo } from './helm/helmChartType'; import { Oc } from './oc/ocWrapper'; import { Component } from './openshift/component'; -import { getServiceKindStubs } from './openshift/serviceHelpers'; +import { getServiceKindStubs, getServices } from './openshift/serviceHelpers'; import { PortForward } from './port-forward'; import { KubeConfigUtils, getKubeConfigFiles, getNamespaceKind } from './util/kubeUtils'; import { LoginUtil } from './util/loginUtil'; @@ -35,7 +35,7 @@ import { Platform } from './util/platform'; import { Progress } from './util/progress'; import { FileContentChangeNotifier, WatchUtil } from './util/watch'; import { vsCommand } from './vscommand'; -import { CustomResourceDefinitionStub } from './webview/common/createServiceTypes'; +import { CustomResourceDefinitionStub, K8sResourceKind } from './webview/common/createServiceTypes'; import { OpenShiftTerminalManager } from './webview/openshift-terminal/openShiftTerminal'; import { getOutputFormat, helmfsUri, kubefsUri } from './k8s/vfs/kuberesources.virtualfs'; @@ -355,6 +355,16 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos // operator framework is not installed on cluster; do nothing } void commands.executeCommand('setContext', 'showCreateService', serviceKinds.length > 0); + + // The 'Create Route' menu visibility + let services: K8sResourceKind[] = []; + try { + services = await getServices(); + } + catch (_) { + // operator framework is not installed on cluster; do nothing + } + void commands.executeCommand('setContext', 'showCreateRoute', services.length > 0); } else if ('kind' in element && element.kind === 'helmContexts') { const helmRepos = { kind: 'helmRepos', @@ -411,6 +421,12 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos name: 'pods' }, } as OpenShiftObject; + const routes = { + kind: 'routes', + metadata: { + name: 'routes' + }, + } as OpenShiftObject; const statefulSets = { kind: 'statefulsets', metadata: { @@ -456,7 +472,7 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos result.push(pods, statefulSets, daemonSets, jobs, cronJobs); if (isOpenshiftCluster) { - result.push(deploymentConfigs, imageStreams, buildConfigs); + result.push(deploymentConfigs, imageStreams, buildConfigs, routes); } } else if ('kind' in element) { const collectableServices: CustomResourceDefinitionStub[] = await this.getServiceKinds(); diff --git a/src/extension.ts b/src/extension.ts index b04f66ee6..04cdce298 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -101,6 +101,7 @@ export async function activate(extensionContext: ExtensionContext): Promise = { value: any; }; +export type Service = { + apiVersion: string; + items: K8sResourceKind[]; +} + export type Error = { message: string }; diff --git a/src/k8s/route.ts b/src/k8s/route.ts index ba45123a4..0bfc9fbd3 100644 --- a/src/k8s/route.ts +++ b/src/k8s/route.ts @@ -2,7 +2,6 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ - import { KubeConfig } from '@kubernetes/client-node'; import { commands, Uri } from 'vscode'; import { Oc } from '../oc/ocWrapper'; diff --git a/src/oc/ocWrapper.ts b/src/oc/ocWrapper.ts index 85e0e114d..c9751b126 100644 --- a/src/oc/ocWrapper.ts +++ b/src/oc/ocWrapper.ts @@ -12,6 +12,7 @@ import { KubeConfigUtils } from '../util/kubeUtils'; import { Platform } from '../util/platform'; import { Project } from './project'; import { ClusterType, KubernetesConsole } from './types'; +import validator from 'validator'; /** * A wrapper around the `oc` CLI tool. @@ -501,6 +502,38 @@ export class Oc { }); } + public async createRoute(routeName: string, serviceName: string, hostName: string, path: string, port: { number: string, name: string, protocol: string, targetPort: string }, + isSecured: boolean): Promise { + let cmdText: CommandText; + if (isSecured) { + + cmdText = new CommandText('oc', `create route edge ${routeName}`, [ + new CommandOption('--service', serviceName), + new CommandOption('--port', port.number), + ]); + + } else { + cmdText = new CommandText('oc', `expose service ${serviceName.trim()}`, [ + new CommandOption('--name', routeName), + new CommandOption('--port', port.number), + new CommandOption('--target-port', port.targetPort), + new CommandOption('--protocol', port.protocol) + ]); + } + + if (!validator.isEmpty(hostName)) { + cmdText.addOption(new CommandOption('--hostname', hostName)); + } + + if (!validator.isEmpty(path)) { + cmdText.addOption(new CommandOption('--path', path)); + } + return await CliChannel.getInstance().executeTool( + cmdText + ) + .then((result) => result.stdout); + } + /** * Changes which project is currently being used. * diff --git a/src/openshift/nameValidator.ts b/src/openshift/nameValidator.ts index 36ce090b0..7f0c7a41f 100644 --- a/src/openshift/nameValidator.ts +++ b/src/openshift/nameValidator.ts @@ -22,6 +22,11 @@ export function validateMatches(message: string, value: string): string | null { return validator.matches(value, '^[a-z]([-a-z0-9]*[a-z0-9])*$') ? null : message; } +export function validatePath(message: string, value: string): string | null { + const pathRegx = value.match(/^(\/{1}(?!\/))[A-Za-z0-9/\-_]*(([a-zA-Z]+))?$/); + return pathRegx ? null : message; +} + export function validateFilePath(message: string, value: string): string | null { const proposedPath = path.parse(value); return /^devfile\.ya?ml$/i.test(proposedPath.base) ? null : message; diff --git a/src/openshift/route.ts b/src/openshift/route.ts new file mode 100644 index 000000000..7c6ad9c1f --- /dev/null +++ b/src/openshift/route.ts @@ -0,0 +1,17 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { vsCommand } from '../vscommand'; +import CreateRouteViewLoader from '../webview/create-route/createRouteViewLoader'; + +/** + * Wraps commands that are used for interacting with routes. + */ +export class Route { + @vsCommand('openshift.route.create') + static async createNewRoute() { + await CreateRouteViewLoader.loadView(); + } +} diff --git a/src/openshift/serviceHelpers.ts b/src/openshift/serviceHelpers.ts index 79277ddde..9b9f5aefe 100644 --- a/src/openshift/serviceHelpers.ts +++ b/src/openshift/serviceHelpers.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ +import { K8sResourceKind } from '../k8s/olm/types'; import { Oc } from '../oc/ocWrapper'; import { ClusterServiceVersion, @@ -21,3 +22,9 @@ export async function getServiceKindStubs(): Promise { + return (await Oc.Instance.getKubernetesObjects( + 'service' + )) as unknown as K8sResourceKind[]; +} diff --git a/src/webview/common-ext/utils.ts b/src/webview/common-ext/utils.ts index e5facea8c..8b3e827e3 100644 --- a/src/webview/common-ext/utils.ts +++ b/src/webview/common-ext/utils.ts @@ -61,8 +61,8 @@ function isGitURL(host: string): boolean { ].includes(host); } -export function validateURL(event: Message): validateURLProps { - if (typeof event.data === 'string' && (event.data).trim().length === 0) { +export function validateURL(event: Message | { command: string; data: object }, isRequired = true): validateURLProps { + if (isRequired && typeof event.data === 'string' && (event.data).trim().length === 0) { return { url: event.data, error: true, @@ -78,7 +78,7 @@ export function validateURL(event: Message): validateURLProps { return { url: event.data, error: false, - helpText: 'URL is valid' + helpText: !isRequired ? '' : 'URL is valid' } as validateURLProps } @@ -119,13 +119,31 @@ export function validateGitURL(event: Message): validateURLProps { } export function validateName(value: string): string | null { - let validationMessage = NameValidator.emptyName('Required', value.trim()); + let validationMessage = NameValidator.emptyName('Required', value); if (!validationMessage) { validationMessage = NameValidator.validateMatches( - 'Only lower case alphabets and numeric characters or \'-\', start and ends with only alphabets', - value, + 'Only lower case alphabets and numeric characters or \'-\', start and ends with only alphabets', value ); } if (!validationMessage) { validationMessage = NameValidator.lengthName('Should be between 2-63 characters', value, 0); } return validationMessage; } + +export function validateJSONValue(value: string): string | null { + let validationMessage = NameValidator.emptyName('Required', JSON.parse(value) as unknown as string); + if (!validationMessage) { + validationMessage = NameValidator.validateMatches( + 'Only lower case alphabets and numeric characters or \'-\', start and ends with only alphabets', + JSON.parse(value) as unknown as string + ); + } + if (!validationMessage) { validationMessage = NameValidator.lengthName('Should be between 2-63 characters', JSON.parse(value) as unknown as string, 0); } + return validationMessage; +} + +export function validatePath(value: string): string | null { + return NameValidator.validatePath( + 'Given path is not valid', + JSON.parse(value) as unknown as string + ); +} diff --git a/src/webview/common/createServiceTypes.ts b/src/webview/common/createServiceTypes.ts index 1b7078994..9bbf7db02 100644 --- a/src/webview/common/createServiceTypes.ts +++ b/src/webview/common/createServiceTypes.ts @@ -64,3 +64,72 @@ export type SpecDescriptor = { */ path: string; } + +export type OwnerReference = { + name: string; + kind: string; + uid: string; + apiVersion: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +}; + +export type ObjectMetadata = { + annotations?: { [key: string]: string }; + clusterName?: string; + creationTimestamp?: string; + deletionGracePeriodSeconds?: number; + deletionTimestamp?: string; + finalizers?: string[]; + generateName?: string; + generation?: number; + labels?: { [key: string]: string }; + managedFields?: any[]; + name?: string; + namespace?: string; + ownerReferences?: OwnerReference[]; + resourceVersion?: string; + uid?: string; +}; + +// Properties common to (almost) all Kubernetes resources. +export type K8sResourceCommon = { + apiVersion?: string; + kind?: string; + metadata?: ObjectMetadata; +}; + +export type MatchExpression = { + key: string; + operator: 'Exists' | 'DoesNotExist' | 'In' | 'NotIn' | 'Equals' | 'NotEqual'; + values?: string[]; + value?: string; +}; + +export type MatchLabels = { + [key: string]: string; +}; + +export type Selector = { + matchLabels?: MatchLabels; + matchExpressions?: MatchExpression[]; +}; + +export type Port = { + name: string; + port: string; + protocol: string; + targetPort: string; +}; + +// Generic, unknown kind. Avoid when possible since it allows any key in spec +// or status, weakening type checking. +export type K8sResourceKind = K8sResourceCommon & { + spec?: { + selector?: Selector | MatchLabels; + ports?: Port[]; + [key: string]: any; + }; + status?: { [key: string]: any }; + data?: { [key: string]: any }; +}; diff --git a/src/webview/common/route.ts b/src/webview/common/route.ts new file mode 100644 index 000000000..fb294eecd --- /dev/null +++ b/src/webview/common/route.ts @@ -0,0 +1,25 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +export type RouteInputBoxText = { + + name: string; + error: boolean; + helpText: string; +} + +export type CreateRoute = { + routeName: string; + hostname: string; + path: string; + serviceName: string; + port: { + number: string; + name: string; + protocal: string; + targetPort: string; + }; + isSecured: boolean; +} diff --git a/src/webview/create-route/app/createForm.tsx b/src/webview/create-route/app/createForm.tsx new file mode 100644 index 000000000..ce2e4d117 --- /dev/null +++ b/src/webview/create-route/app/createForm.tsx @@ -0,0 +1,369 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ +import { + Box, + Container, + FormControl, + PaletteMode, + Stack, + ThemeProvider, + Typography, + TextField, + FormHelperText, + MenuItem, + Select, + InputLabel, + Checkbox, + FormControlLabel, + Button +} from '@mui/material'; +import * as React from 'react'; +import 'react-dom'; +import type { K8sResourceKind, Port } from '../../common/createServiceTypes'; +import type { RouteInputBoxText } from '../../common/route'; +import { LoadScreen } from '../../common/loading'; +import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt'; +import { createVSCodeTheme } from '../../common/vscode-theme'; +import { ErrorPage } from '../../common/errorPage'; + +/** + * Component to select which type of service (which CRD) should be created. + */ +function SelectService(props: { + routeNameObj: RouteInputBoxText; + setRouteNameObj; + hostNameObj: RouteInputBoxText; + setHostNameObj; + pathObj: RouteInputBoxText; + setPathNameObj; + serviceKinds: K8sResourceKind[]; + selectedServiceKind: K8sResourceKind; + setSelectedServiceKind; + ports: Port[], + setPorts; + selectedPort: Port; + setSelectedPort; +}) { + + const [isServiceKindTouched, setServiceKindTouched] = React.useState(false); + const [isPortTouched, setPortTouched] = React.useState(false); + const [isSecured, setSecured] = React.useState(false); + + return ( +
{ + event.preventDefault(); + }} + > + + + Create Route + + { + window.vscodeApi.postMessage({ + command: 'validateRouteName', + data: e.target.value + }); + props.setRouteNameObj((prevState: RouteInputBoxText) => ({ ...prevState, name: e.target.value })); + }} + /> + { + window.vscodeApi.postMessage({ + command: 'validateHost', + data: e.target.value + }); + props.setHostNameObj((prevState: RouteInputBoxText) => ({ ...prevState, name: e.target.value })); + }} /> + { + window.vscodeApi.postMessage({ + command: 'validatePath', + data: e.target.value + }); + props.setPathNameObj((prevState: RouteInputBoxText) => ({ ...prevState, name: e.target.value })); + }} /> + + Service + + Service to route to. + + + Target Port + + Target Port for traffic + + + { + setSecured((isSecured) => !isSecured); + }} /> + } + label='Secure Route' + /> + Routes can be secured using several TLS termination types for serving certificates. + + + + + + +
+ ); +} + +type CreateServicePage = 'Loading' | 'PickServiceKind' | 'Error'; + +export function CreateService() { + const [page, setPage] = React.useState('Loading'); + + const [themeKind, setThemeKind] = React.useState('light'); + const theme = React.useMemo(() => createVSCodeTheme(themeKind), [themeKind]); + const [error, setError] = React.useState(undefined); + + const [routeNameObj, setRouteNameObj] = React.useState({ + name: '', + error: false, + helpText: 'A unique name for the Route within the project.' + }); + + const [hostNameObj, setHostNameObj] = React.useState({ + name: '', + error: false, + helpText: 'Public host name for the Route. If not specified, a hostname is generated.' + }); + + const [pathObj, setPathObj] = React.useState({ + name: '', + error: false, + helpText: 'Path that the router watches to route traffic to the service.' + }); + + const [serviceKinds, setServiceKinds] = React.useState(undefined); + const [selectedServiceKind, setSelectedServiceKind] = + React.useState(undefined); + const [ports, setPorts] = React.useState([]); + const [selectedPort, setSelectedPort] = + React.useState(undefined); + + function messageListener(event) { + if (event?.data) { + const message = event.data; + switch (message.action) { + case 'setTheme': + setThemeKind(event.data.themeValue === 1 ? 'light' : 'dark'); + break; + case 'setServiceKinds': + setServiceKinds((_) => message.data); + setPage((_) => 'PickServiceKind'); + break; + case 'validateRouteName': { + const routeData: RouteInputBoxText = JSON.parse(message.data) as unknown as RouteInputBoxText; + setRouteNameObj({ + name: routeData.name, + error: routeData.error, + helpText: routeData.helpText !== '' ? routeData.helpText : routeNameObj.helpText + }); + break; + + } + case 'validateHost': { + const hostData: RouteInputBoxText = JSON.parse(message.data) as unknown as RouteInputBoxText; + setHostNameObj({ + name: hostData.name, + error: hostData.error, + helpText: hostData.helpText !== '' ? hostData.helpText : hostNameObj.helpText + }); + break; + } + case 'validatePath': { + const PathData: RouteInputBoxText = JSON.parse(message.data) as unknown as RouteInputBoxText; + setPathObj({ + name: PathData.name, + error: PathData.error, + helpText: PathData.helpText + }); + break; + } + case 'error': + setError(() => message.data) + setPage(() => 'Error'); + break; + default: + break; + } + } + } + + React.useEffect(() => { + window.addEventListener('message', messageListener); + return () => { + window.removeEventListener('message', messageListener); + }; + }, []); + + let pageElement; + + switch (page) { + case 'Loading': + return ; + case 'Error': + pageElement = (); + break; + case 'PickServiceKind': + pageElement = ( + + ); + break; + default: + <>Error; + } + + return ( + + + {pageElement} + {error?.trim().length > 0 && + + + + } + + + ); +} diff --git a/src/webview/create-route/app/index.html b/src/webview/create-route/app/index.html new file mode 100644 index 000000000..e01f843fd --- /dev/null +++ b/src/webview/create-route/app/index.html @@ -0,0 +1,30 @@ + + + + + + + Create Service View + + + + + +
+ + + diff --git a/src/webview/create-route/app/index.tsx b/src/webview/create-route/app/index.tsx new file mode 100644 index 000000000..01ac6d973 --- /dev/null +++ b/src/webview/create-route/app/index.tsx @@ -0,0 +1,15 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import * as ReactDOM from 'react-dom'; +import * as React from 'react'; +import { CreateService } from './createForm'; +import { WebviewErrorBoundary } from '../../common/webviewErrorBoundary'; + +ReactDOM.render(( + + + +), document.getElementById('root')); diff --git a/src/webview/create-route/app/tsconfig.json b/src/webview/create-route/app/tsconfig.json new file mode 100644 index 000000000..cebd3bc24 --- /dev/null +++ b/src/webview/create-route/app/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "module": "esnext", + "moduleResolution": "node", + "target": "es6", + "outDir": "creatRouteView", + "lib": [ + "es6", + "dom" + ], + "jsx": "react", + "sourceMap": true, + "noUnusedLocals": true, + // "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "experimentalDecorators": true, + "typeRoots": [ + "../../../../node_modules/@types", + "../../@types" + ], + "baseUrl": ".", + "skipLibCheck": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/src/webview/create-route/createRouteViewLoader.ts b/src/webview/create-route/createRouteViewLoader.ts new file mode 100644 index 000000000..85d03c60f --- /dev/null +++ b/src/webview/create-route/createRouteViewLoader.ts @@ -0,0 +1,203 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ +import * as path from 'path'; +import * as vscode from 'vscode'; +import { OpenShiftExplorer } from '../../explorer'; +import { Oc } from '../../oc/ocWrapper'; +import { ExtensionID } from '../../util/constants'; +import { loadWebviewHtml, validateJSONValue, validatePath, validateURL } from '../common-ext/utils'; +import { getServices as getService } from '../../openshift/serviceHelpers'; +import type { CreateRoute } from '../common/route'; + +export default class CreateRouteViewLoader { + private static panel: vscode.WebviewPanel; + + static get extensionPath(): string { + return vscode.extensions.getExtension(ExtensionID).extensionPath; + } + + static async loadView(): Promise { + const localResourceRoot = vscode.Uri.file( + path.join(CreateRouteViewLoader.extensionPath, 'out', 'create-route', 'app'), + ); + + if (CreateRouteViewLoader.panel) { + CreateRouteViewLoader.panel.reveal(); + return CreateRouteViewLoader.panel; + } + + CreateRouteViewLoader.panel = vscode.window.createWebviewPanel( + 'createRouteView', + 'Create Route', + vscode.ViewColumn.One, + { + enableScripts: true, + localResourceRoots: [localResourceRoot], + retainContextWhenHidden: true, + }, + ); + + CreateRouteViewLoader.panel.iconPath = vscode.Uri.file( + path.join(CreateRouteViewLoader.extensionPath, 'images/context/cluster-node.png'), + ); + CreateRouteViewLoader.panel.webview.html = await loadWebviewHtml( + 'create-route', + CreateRouteViewLoader.panel, + ); + + const colorThemeDisposable = vscode.window.onDidChangeActiveColorTheme(async function ( + colorTheme: vscode.ColorTheme, + ) { + await CreateRouteViewLoader.panel.webview.postMessage({ + action: 'setTheme', + themeValue: colorTheme.kind, + }); + }); + + CreateRouteViewLoader.panel.onDidDispose(() => { + colorThemeDisposable.dispose(); + CreateRouteViewLoader.panel = undefined; + }); + + CreateRouteViewLoader.panel.onDidDispose(() => { + CreateRouteViewLoader.panel = undefined; + }); + CreateRouteViewLoader.panel.webview.onDidReceiveMessage( + CreateRouteViewLoader.messageListener, + ); + return CreateRouteViewLoader.panel; + } + + static async messageListener(message: { command: string; data: object }): Promise { + switch (message.command) { + case 'ready': + try { + // set theme + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'setTheme', + themeValue: vscode.window.activeColorTheme.kind, + }); + // send list of possible kinds of service to create + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'setServiceKinds', + data: await getService(), + }); + } catch (e) { + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'error', + data: `${e}`, + }); + void vscode.window.showErrorMessage(`${e}`); + } + break; + case 'getSpec': { + try { + const services = await getService(); + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'setSpec', + data: { + services + }, + }); + } catch (e) { + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'error', + data: `${e}`, + }); + void vscode.window.showErrorMessage(`${e}`); + } + break; + } + case 'create': { + try { + const route: CreateRoute = message.data as CreateRoute; + const port = { + name: route.port.name, + number: route.port.number, + protocol: route.port.protocal, + targetPort: route.port.targetPort + } + await Oc.Instance.createRoute(route.routeName, route.serviceName, route.hostname, route.path, port, route.isSecured); + void vscode.window.showInformationMessage(`Route ${route.routeName} successfully created.`); + CreateRouteViewLoader.panel.dispose(); + CreateRouteViewLoader.panel = undefined; + OpenShiftExplorer.getInstance().refresh(); + } catch (err) { + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'error', + data: `${err}`, + }); + void vscode.window.showErrorMessage(err); + } + break; + } + case 'close': { + CreateRouteViewLoader.panel.dispose(); + CreateRouteViewLoader.panel = undefined; + break; + } + case 'validateRouteName': { + const flag = validateJSONValue(JSON.stringify(message.data)); + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'validateRouteName', + data: JSON.stringify({ + error: !flag ? false : true, + helpText: !flag ? '' : flag, + name: message.data + }) + }); + break; + } + case 'validateHost': { + if (JSON.stringify(message.data).trim() === '') { + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'validateHost', + data: JSON.stringify({ + error: false, + helpText: '', + name: message.data + }) + }); + break; + } + const flag = validateURL(message, false); + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'validateHost', + data: JSON.stringify({ + error: !flag.error ? false : true, + helpText: flag.helpText, + name: message.data + }) + }); + break; + } + case 'validatePath': { + if (JSON.stringify(message.data).trim() === '') { + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'validatePath', + data: JSON.stringify({ + error: false, + helpText: '', + name: message.data + }) + }); + break; + } + const flag = validatePath(JSON.stringify(message.data)); + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'validatePath', + data: JSON.stringify({ + error: !flag ? false : true, + helpText: !flag ? '' : flag, + name: message.data + }) + }); + break; + } + default: + void vscode.window.showErrorMessage(`Unrecognized message ${message.command}`); + } + } +} diff --git a/tsconfig.json b/tsconfig.json index 46ef9c25c..aed0112ce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,6 +31,7 @@ "test-resources", "src/webview/cluster/app", "src/webview/create-service/app", + "src/webview/create-route/app", "src/webview/create-component/app", "src/webview/create-component/pages", "src/webview/devfile-registry/app",