From 8f6dcc62a189ea83a48078c2fc820bc5ec63b06b Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Mon, 20 May 2024 00:43:17 +0530 Subject: [PATCH 1/7] create route functionality added --- build/esbuild.mjs | 1 + package.json | 10 + src/explorer.ts | 14 +- src/extension.ts | 1 + src/k8s/olm/types.ts | 5 + src/openshift/route.ts | 17 + src/openshift/serviceHelpers.ts | 7 + src/webview/common-ext/utils.ts | 6 +- src/webview/common/createServiceTypes.ts | 73 +++ src/webview/common/route.ts | 11 + src/webview/create-route/app/createForm.tsx | 521 ++++++++++++++++++ src/webview/create-route/app/index.html | 30 + src/webview/create-route/app/index.tsx | 15 + src/webview/create-route/app/tsconfig.json | 27 + .../create-route/createRouteViewLoader.ts | 164 ++++++ tsconfig.json | 1 + 16 files changed, 898 insertions(+), 5 deletions(-) create mode 100644 src/openshift/route.ts create mode 100644 src/webview/common/route.ts create mode 100644 src/webview/create-route/app/createForm.tsx create mode 100644 src/webview/create-route/app/index.html create mode 100644 src/webview/create-route/app/index.tsx create mode 100644 src/webview/create-route/app/tsconfig.json create mode 100644 src/webview/create-route/createRouteViewLoader.ts 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 a1e4aa5c8..33c7db216 100644 --- a/package.json +++ b/package.json @@ -810,6 +810,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", @@ -1621,6 +1626,11 @@ "when": "view == openshiftProjectExplorer && isLoggedIn && viewItem =~ /openshift.project.*/i && showCreateService", "group": "c2" }, + { + "command": "openshift.route.create", + "when": "view == openshiftProjectExplorer && isLoggedIn && viewItem =~ /openshift.project.*/i && 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 de76815d7..26587e562 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -27,13 +27,13 @@ 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 { KubeConfigUtils, getKubeConfigFiles, getNamespaceKind } from './util/kubeUtils'; 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 { LoginUtil } from './util/loginUtil'; @@ -339,6 +339,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', diff --git a/src/extension.ts b/src/extension.ts index 4fa45e2f9..e9057e0db 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -94,6 +94,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/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..f8efc8e18 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 } diff --git a/src/webview/common/createServiceTypes.ts b/src/webview/common/createServiceTypes.ts index 1b7078994..14367ccaf 100644 --- a/src/webview/common/createServiceTypes.ts +++ b/src/webview/common/createServiceTypes.ts @@ -64,3 +64,76 @@ 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: number; + 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..f412ed6d7 --- /dev/null +++ b/src/webview/common/route.ts @@ -0,0 +1,11 @@ +/*----------------------------------------------------------------------------------------------- + * 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; +} diff --git a/src/webview/create-route/app/createForm.tsx b/src/webview/create-route/app/createForm.tsx new file mode 100644 index 000000000..8236d1e2a --- /dev/null +++ b/src/webview/create-route/app/createForm.tsx @@ -0,0 +1,521 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ +import ExpandLess from '@mui/icons-material/ExpandLess'; +import ExpandMore from '@mui/icons-material/ExpandMore'; +import { + Box, + Collapse, + Container, + FormControl, + IconButton, + PaletteMode, + Paper, + Stack, + ThemeProvider, + Typography, + TextField, + Grid, + FormHelperText, + MenuItem, + Select, + InputLabel, + Checkbox, + FormControlLabel +} from '@mui/material'; +import Form from '@rjsf/mui'; +import type { + ObjectFieldTemplateProps, + TitleFieldProps, + ArrayFieldTemplateProps, + RJSFSchema, + StrictRJSFSchema, + FormContextType, + ArrayFieldTemplateItemType +} from '@rjsf/utils'; +import { getTemplate, getUiOptions } from '@rjsf/utils'; +import validator from '@rjsf/validator-ajv8'; +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 { createVSCodeTheme } from '../../common/vscode-theme'; +import { ArrowBack } from '@mui/icons-material'; +import { ErrorPage } from '../../common/errorPage'; + +/** + * A replacement for the RJSF object field component that resembles the one in Patternfly and allows collapsing. + */ +function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { + const [isExpanded, setExpanded] = React.useState(true); + + return ( + <> + {props.title ? ( + <> + + + { + e.preventDefault(); + setExpanded(!isExpanded); + }} + > + {isExpanded ? : } + + + + + {props.title} + {props.required && ' *'} + + + {props.description} + + + + + + + {props.properties.map((element) => ( +
{element.content}
+ ))} +
+
+
+ + ) : ( + <> + + {props.properties.map((element) => ( +
{element.content}
+ ))} +
+ + )} + + ); +} + +/** + * Based on https://github.com/rjsf-team/react-jsonschema-form/blob/main/packages/mui/src/ArrayFieldTemplate/ArrayFieldTemplate.tsx + */ +function ArrayFieldTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: ArrayFieldTemplateProps) { + const { + canAdd, + disabled, + uiSchema, + items, + onAddClick, + schema, + readonly, + registry, + required, + title, + } = props; + + const uiOptions = getUiOptions(uiSchema); + const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>( + 'ArrayFieldItemTemplate', + registry, + uiOptions, + ); + + const { + ButtonTemplates: { AddButton }, + } = registry.templates; + return ( + + + + + {title} + {required && ' *'} + + + {schema.description} + + + {items && + items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType) => ( + + ))} + {canAdd && ( + + + + + + + + )} + + + ); +} + +function TitleFieldTemplate(props: TitleFieldProps) { + return ( + <> +

+ {props.title} + {props.required && '*'} +

+ + ); +} + +/** + * Component to select which type of service (which CRD) should be created. + */ +function LoadForm(props: { + routeNameObj: RouteInputBoxText; + hostNameObj: RouteInputBoxText; + pathObj: RouteInputBoxText; + serviceKinds: K8sResourceKind[]; + selectedServiceKind: K8sResourceKind; + setSelectedServiceKind; + selectedPort: Port; + setSelectedPort; +}) { + + const [isServiceKindTouched, setServiceKindTouched] = React.useState(false); + const [isPortTouched, setPortTouched] = React.useState(false); + const [isSecured, setSecured] = React.useState(false); + const [ports, setPorts] = React.useState([]); + + return ( +
{ + event.preventDefault(); + }} + > + + + Create Route + + { + window.vscodeApi.postMessage({ + command: 'validateRouteName', + data: e.target.value + }); + }} /> + { + window.vscodeApi.postMessage({ + command: 'validateHostName', + data: e.target.value + }); + }} /> + { + window.vscodeApi.postMessage({ + command: 'validateHostName', + data: 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. + + +
+ ); +} + +/** + * Component to set the required fields for the selected CRD using an RJSF form. + */ +function SpecifyService(props: { + serviceKind: K8sResourceKind; + spec: object; + defaults: object; + next: () => void; + back: () => void; +}) { + const [formData, setFormData] = React.useState(props.defaults); + + const onSubmit = (_data, event: React.FormEvent): void => { + event.preventDefault(); + window.vscodeApi.postMessage({ + command: 'create', + data: formData, + }); + props.next(); + }; + + return ( + + + + + + Create {props.serviceKind.kind} + +
setFormData((_) => e.formData)} + onSubmit={onSubmit} + liveValidate + noHtml5Validate + validator={validator} + showErrorList='top' + templates={{ ObjectFieldTemplate, TitleFieldTemplate, ArrayFieldTemplate }} + >
+
+ ); +} + +type CreateServicePage = 'Loading' | 'PickServiceKind' | 'ConfigureService' | 'Error'; + +export function CreateService() { + const [page, setPage] = React.useState('Loading'); + const [spec, setSpec] = React.useState(undefined); + const [defaults, setDefaults] = React.useState(undefined); + + 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 [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 'setSpec': + setSpec(message.data.spec); + setDefaults(message.data.defaults); + setPage('ConfigureService'); + break; + case 'validateRouteName': + setRouteNameObj({ + name: message.data.name, + error: message.data.error, + helpText: message.data.helpText !== '' ? message.data.helpText : routeNameObj.helpText + }); + break; + case 'validateHostName': + setHostNameObj({ + name: message.data.name, + error: message.data.error, + helpText: message.data.helpText !== '' ? message.data.helpText : hostNameObj.helpText + }); + break; + case 'validatePath': + setPathObj({ + name: message.data.name, + error: message.data.error, + helpText: message.data.helpText + }); + break; + case 'error': + setError((prev) => message.data) + setPage((prev) => 'Error'); + break; + default: + break; + } + } + } + + React.useEffect(() => { + window.addEventListener('message', messageListener); + return () => { + window.removeEventListener('message', messageListener); + }; + }, []); + + let pageElement; + + switch (page) { + case 'Loading': + return ; + case 'Error': + return ; + case 'PickServiceKind': + pageElement = ( + + ); + break; + case 'ConfigureService': + pageElement = ( + { + setPage('Loading'); + }} + back={() => { + setPage('PickServiceKind'); + }} + /> + ); + break; + default: + <>Error; + } + + return ( + + {pageElement} + + ); +} 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..fb2e9da2b --- /dev/null +++ b/src/webview/create-route/createRouteViewLoader.ts @@ -0,0 +1,164 @@ +/*----------------------------------------------------------------------------------------------- + * 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 _ from 'lodash'; +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, validateName, validateURL } from '../common-ext/utils'; +import { getServices as getService } from '../../openshift/serviceHelpers'; + +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(); + console.log(services); + 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 { + await Oc.Instance.createKubernetesObjectFromSpec(message.data); + void vscode.window.showInformationMessage(`Service ${(message.data as unknown as any).metadata.name} 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 'validateRouteName': { + const flag = validateName(message.data.toString()); + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'validateRouteName', + data: { + error: !flag ? false : true, + helpText: !flag ? '' : flag, + name: message.data.toString() + } + }); + break; + } + case 'validateHostName': { + const flag = validateURL(message, false); + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'validateHostName', + data: { + error: !flag.error ? false : true, + helpText: flag.helpText, + name: message.data.toString() + } + }); + 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", From 90e571619ab6056a74281aed995c21e6501f879d Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Wed, 22 May 2024 13:51:07 +0530 Subject: [PATCH 2/7] added secure and un secured routes --- package-lock.json | 19 ++ package.json | 1 + src/explorer.ts | 8 +- src/oc/ocWrapper.ts | 32 ++ src/openshift/nameValidator.ts | 5 + src/webview/common-ext/utils.ts | 7 + src/webview/common/createServiceTypes.ts | 2 +- src/webview/common/route.ts | 15 +- src/webview/create-route/app/createForm.tsx | 277 +++--------------- .../create-route/createRouteViewLoader.ts | 51 +++- 10 files changed, 174 insertions(+), 243 deletions(-) diff --git a/package-lock.json b/package-lock.json index f62b83a65..4710e1e63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,6 +114,7 @@ "survey-core": "^1.10.2", "survey-react-ui": "^1.10.2", "typescript": "^5.4.5", + "valid-path": "^2.1.0", "vscode-extension-tester": "^8.1", "xterm-addon-fit": "^0.8.0", "xterm-addon-web-links": "^0.9.0", @@ -17643,6 +17644,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/valid-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/valid-path/-/valid-path-2.1.0.tgz", + "integrity": "sha512-hYanLM6kqE7zLl0oykV//2q3meRgYGOtS2lgChozuWjjghSqR8xwc3199bDGUFPwszk/MhvHhyVys8HdHU9buw==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -29826,6 +29836,15 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true }, + "valid-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/valid-path/-/valid-path-2.1.0.tgz", + "integrity": "sha512-hYanLM6kqE7zLl0oykV//2q3meRgYGOtS2lgChozuWjjghSqR8xwc3199bDGUFPwszk/MhvHhyVys8HdHU9buw==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 33c7db216..2a2434064 100644 --- a/package.json +++ b/package.json @@ -176,6 +176,7 @@ "survey-core": "^1.10.2", "survey-react-ui": "^1.10.2", "typescript": "^5.4.5", + "valid-path": "^2.1.0", "vscode-extension-tester": "^8.1", "xterm-addon-fit": "^0.8.0", "xterm-addon-web-links": "^0.9.0", diff --git a/src/explorer.ts b/src/explorer.ts index 26587e562..59832804b 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -400,6 +400,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: { @@ -442,7 +448,7 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos name: 'build configs' }, } as OpenShiftObject; - result.push(pods, + result.push(pods, routes, statefulSets, daemonSets, jobs, cronJobs); if (isOpenshiftCluster) { result.push(deploymentConfigs, imageStreams, buildConfigs); diff --git a/src/oc/ocWrapper.ts b/src/oc/ocWrapper.ts index 77275b056..9da232105 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. @@ -482,6 +483,37 @@ export class Oc { }); } + public async createRoute(routeName: string, serviceName: string, hostName: string, path: string, port: { number: string, name: string, protocol: 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('--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..3cea84915 100644 --- a/src/openshift/nameValidator.ts +++ b/src/openshift/nameValidator.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import validator from 'validator'; +import validPath = require('valid-path'); export function emptyName(message: string, value: string): string | null { return validator.isEmpty(value) ? message : null; @@ -22,6 +23,10 @@ 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 { + return validPath(value).valid ? 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/webview/common-ext/utils.ts b/src/webview/common-ext/utils.ts index f8efc8e18..af1ed36a2 100644 --- a/src/webview/common-ext/utils.ts +++ b/src/webview/common-ext/utils.ts @@ -129,3 +129,10 @@ export function validateName(value: string): string | null { if (!validationMessage) { validationMessage = NameValidator.lengthName('Should be between 2-63 characters', value, 0); } return validationMessage; } + +export function validatePath(value: string): string | null { + return NameValidator.validatePath( + 'Given path is not valid', + value, + ); +} diff --git a/src/webview/common/createServiceTypes.ts b/src/webview/common/createServiceTypes.ts index 14367ccaf..744d30451 100644 --- a/src/webview/common/createServiceTypes.ts +++ b/src/webview/common/createServiceTypes.ts @@ -121,7 +121,7 @@ export type Selector = { export type Port = { name: string; - port: number; + port: string; protocol: string; targetPort: string; }; diff --git a/src/webview/common/route.ts b/src/webview/common/route.ts index f412ed6d7..b448d5574 100644 --- a/src/webview/common/route.ts +++ b/src/webview/common/route.ts @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. + * Copyright (c) Red Hat; Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ @@ -9,3 +9,16 @@ export type RouteInputBoxText = { error: boolean; helpText: string; } + +export type CreateRoute = { + routeName: string; + hostname: string; + path: string; + serviceName: string; + port: { + number: string; + name: string; + protocal: string + }; + isSecured: boolean; +} diff --git a/src/webview/create-route/app/createForm.tsx b/src/webview/create-route/app/createForm.tsx index 8236d1e2a..c19a04cd3 100644 --- a/src/webview/create-route/app/createForm.tsx +++ b/src/webview/create-route/app/createForm.tsx @@ -2,195 +2,36 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import ExpandLess from '@mui/icons-material/ExpandLess'; -import ExpandMore from '@mui/icons-material/ExpandMore'; import { Box, - Collapse, Container, FormControl, - IconButton, PaletteMode, - Paper, Stack, ThemeProvider, Typography, TextField, - Grid, FormHelperText, MenuItem, Select, InputLabel, Checkbox, - FormControlLabel + FormControlLabel, + Button } from '@mui/material'; -import Form from '@rjsf/mui'; -import type { - ObjectFieldTemplateProps, - TitleFieldProps, - ArrayFieldTemplateProps, - RJSFSchema, - StrictRJSFSchema, - FormContextType, - ArrayFieldTemplateItemType -} from '@rjsf/utils'; -import { getTemplate, getUiOptions } from '@rjsf/utils'; -import validator from '@rjsf/validator-ajv8'; 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 { createVSCodeTheme } from '../../common/vscode-theme'; -import { ArrowBack } from '@mui/icons-material'; import { ErrorPage } from '../../common/errorPage'; -/** - * A replacement for the RJSF object field component that resembles the one in Patternfly and allows collapsing. - */ -function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { - const [isExpanded, setExpanded] = React.useState(true); - - return ( - <> - {props.title ? ( - <> - - - { - e.preventDefault(); - setExpanded(!isExpanded); - }} - > - {isExpanded ? : } - - - - - {props.title} - {props.required && ' *'} - - - {props.description} - - - - - - - {props.properties.map((element) => ( -
{element.content}
- ))} -
-
-
- - ) : ( - <> - - {props.properties.map((element) => ( -
{element.content}
- ))} -
- - )} - - ); -} - -/** - * Based on https://github.com/rjsf-team/react-jsonschema-form/blob/main/packages/mui/src/ArrayFieldTemplate/ArrayFieldTemplate.tsx - */ -function ArrayFieldTemplate< - T = any, - S extends StrictRJSFSchema = RJSFSchema, - F extends FormContextType = any, ->(props: ArrayFieldTemplateProps) { - const { - canAdd, - disabled, - uiSchema, - items, - onAddClick, - schema, - readonly, - registry, - required, - title, - } = props; - - const uiOptions = getUiOptions(uiSchema); - const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>( - 'ArrayFieldItemTemplate', - registry, - uiOptions, - ); - - const { - ButtonTemplates: { AddButton }, - } = registry.templates; - return ( - - - - - {title} - {required && ' *'} - - - {schema.description} - - - {items && - items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType) => ( - - ))} - {canAdd && ( - - - - - - - - )} - - - ); -} - -function TitleFieldTemplate(props: TitleFieldProps) { - return ( - <> -

- {props.title} - {props.required && '*'} -

- - ); -} /** * Component to select which type of service (which CRD) should be created. */ -function LoadForm(props: { +function SelectService(props: { routeNameObj: RouteInputBoxText; hostNameObj: RouteInputBoxText; pathObj: RouteInputBoxText; @@ -241,7 +82,7 @@ function LoadForm(props: { helperText={props.hostNameObj.helpText} onChange={(e) => { window.vscodeApi.postMessage({ - command: 'validateHostName', + command: 'validateHost', data: e.target.value }); }} /> @@ -255,7 +96,7 @@ function LoadForm(props: { helperText={props.pathObj.helpText} onChange={(e) => { window.vscodeApi.postMessage({ - command: 'validateHostName', + command: 'validatePath', data: e.target.value }); }} /> @@ -336,61 +177,47 @@ function LoadForm(props: { /> Routes can be secured using several TLS termination types for serving certificates. - - - ); -} - -/** - * Component to set the required fields for the selected CRD using an RJSF form. - */ -function SpecifyService(props: { - serviceKind: K8sResourceKind; - spec: object; - defaults: object; - next: () => void; - back: () => void; -}) { - const [formData, setFormData] = React.useState(props.defaults); - - const onSubmit = (_data, event: React.FormEvent): void => { - event.preventDefault(); - window.vscodeApi.postMessage({ - command: 'create', - data: formData, - }); - props.next(); - }; + + + + -
setFormData((_) => e.formData)} - onSubmit={onSubmit} - liveValidate - noHtml5Validate - validator={validator} - showErrorList='top' - templates={{ ObjectFieldTemplate, TitleFieldTemplate, ArrayFieldTemplate }} - >
- + ); } -type CreateServicePage = 'Loading' | 'PickServiceKind' | 'ConfigureService' | 'Error'; +type CreateServicePage = 'Loading' | 'PickServiceKind' | 'Error'; export function CreateService() { const [page, setPage] = React.useState('Loading'); - const [spec, setSpec] = React.useState(undefined); - const [defaults, setDefaults] = React.useState(undefined); const [themeKind, setThemeKind] = React.useState('light'); const theme = React.useMemo(() => createVSCodeTheme(themeKind), [themeKind]); @@ -431,11 +258,6 @@ export function CreateService() { setServiceKinds((_) => message.data); setPage((_) => 'PickServiceKind'); break; - case 'setSpec': - setSpec(message.data.spec); - setDefaults(message.data.defaults); - setPage('ConfigureService'); - break; case 'validateRouteName': setRouteNameObj({ name: message.data.name, @@ -443,7 +265,7 @@ export function CreateService() { helpText: message.data.helpText !== '' ? message.data.helpText : routeNameObj.helpText }); break; - case 'validateHostName': + case 'validateHost': setHostNameObj({ name: message.data.name, error: message.data.error, @@ -458,8 +280,8 @@ export function CreateService() { }); break; case 'error': - setError((prev) => message.data) - setPage((prev) => 'Error'); + setError(() => message.data) + setPage(() => 'Error'); break; default: break; @@ -483,7 +305,7 @@ export function CreateService() { return ; case 'PickServiceKind': pageElement = ( - ); break; - case 'ConfigureService': - pageElement = ( - { - setPage('Loading'); - }} - back={() => { - setPage('PickServiceKind'); - }} - /> - ); - break; default: <>Error; } diff --git a/src/webview/create-route/createRouteViewLoader.ts b/src/webview/create-route/createRouteViewLoader.ts index fb2e9da2b..b4d7a7aef 100644 --- a/src/webview/create-route/createRouteViewLoader.ts +++ b/src/webview/create-route/createRouteViewLoader.ts @@ -8,8 +8,9 @@ import * as vscode from 'vscode'; import { OpenShiftExplorer } from '../../explorer'; import { Oc } from '../../oc/ocWrapper'; import { ExtensionID } from '../../util/constants'; -import { loadWebviewHtml, validateName, validateURL } from '../common-ext/utils'; +import { loadWebviewHtml, validateName, 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; @@ -113,8 +114,14 @@ export default class CreateRouteViewLoader { } case 'create': { try { - await Oc.Instance.createKubernetesObjectFromSpec(message.data); - void vscode.window.showInformationMessage(`Service ${(message.data as unknown as any).metadata.name} successfully created.` ); + const route: CreateRoute = message.data as CreateRoute; + const port = { + name: route.port.name, + number: route.port.number, + protocol: route.port.protocal + } + 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(); @@ -139,10 +146,21 @@ export default class CreateRouteViewLoader { }); break; } - case 'validateHostName': { + case 'validateHost': { + if (message.data.toString().trim() === '') { + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'validateHost', + data: { + error: false, + helpText: '', + name: message.data.toString() + } + }); + break; + } const flag = validateURL(message, false); void CreateRouteViewLoader.panel.webview.postMessage({ - action: 'validateHostName', + action: 'validateHost', data: { error: !flag.error ? false : true, helpText: flag.helpText, @@ -151,6 +169,29 @@ export default class CreateRouteViewLoader { }); break; } + case 'validatePath': { + if (message.data.toString().trim() === '') { + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'validatePath', + data: { + error: false, + helpText: '', + name: message.data.toString() + } + }); + break; + } + const flag = validatePath(message.data.toString()); + void CreateRouteViewLoader.panel.webview.postMessage({ + action: 'validatePath', + data: { + error: !flag ? false : true, + helpText: !flag ? '' : flag, + name: message.data.toString() + } + }); + break; + } default: void vscode.window.showErrorMessage(`Unrecognized message ${message.command}`); } From 8927cbf1a84899288e57f9462ac0c259603b2216 Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Wed, 22 May 2024 14:42:08 +0530 Subject: [PATCH 3/7] fix lint issues --- src/k8s/route.ts | 1 - src/webview/create-route/app/createForm.tsx | 24 +++++++++++-------- .../create-route/createRouteViewLoader.ts | 24 +++++++++---------- 3 files changed, 25 insertions(+), 24 deletions(-) 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/webview/create-route/app/createForm.tsx b/src/webview/create-route/app/createForm.tsx index c19a04cd3..f3cd8e1f1 100644 --- a/src/webview/create-route/app/createForm.tsx +++ b/src/webview/create-route/app/createForm.tsx @@ -24,6 +24,7 @@ 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'; @@ -158,7 +159,7 @@ function SelectService(props: { > {ports.map((portObj: Port) => ( - {portObj.port} ​→ {portObj.targetPort} ({portObj.protocol}) + {portObj.port} {portObj.targetPort} ({portObj.protocol}) ))} @@ -259,24 +260,27 @@ export function CreateService() { setPage((_) => 'PickServiceKind'); break; case 'validateRouteName': + const routeData = JSON.parse(message.data); setRouteNameObj({ - name: message.data.name, - error: message.data.error, - helpText: message.data.helpText !== '' ? message.data.helpText : routeNameObj.helpText + name: routeData.name, + error: routeData.error, + helpText: routeData.helpText !== '' ? routeData.helpText : routeNameObj.helpText }); break; case 'validateHost': + const hostData = JSON.parse(message.data); setHostNameObj({ - name: message.data.name, - error: message.data.error, - helpText: message.data.helpText !== '' ? message.data.helpText : hostNameObj.helpText + name: hostData.name, + error: hostData.error, + helpText: hostData.helpText !== '' ? hostData.helpText : hostNameObj.helpText }); break; case 'validatePath': + const PathData = JSON.parse(message.data); setPathObj({ - name: message.data.name, - error: message.data.error, - helpText: message.data.helpText + name: PathData.name, + error: PathData.error, + helpText: PathData.helpText }); break; case 'error': diff --git a/src/webview/create-route/createRouteViewLoader.ts b/src/webview/create-route/createRouteViewLoader.ts index b4d7a7aef..375b8fb37 100644 --- a/src/webview/create-route/createRouteViewLoader.ts +++ b/src/webview/create-route/createRouteViewLoader.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 * as _ from 'lodash'; import * as path from 'path'; import * as vscode from 'vscode'; import { OpenShiftExplorer } from '../../explorer'; @@ -96,7 +95,6 @@ export default class CreateRouteViewLoader { case 'getSpec': { try { const services = await getService(); - console.log(services); void CreateRouteViewLoader.panel.webview.postMessage({ action: 'setSpec', data: { @@ -121,7 +119,7 @@ export default class CreateRouteViewLoader { protocol: route.port.protocal } await Oc.Instance.createRoute(route.routeName, route.serviceName, route.hostname, route.path, port, route.isSecured); - void vscode.window.showInformationMessage(`Route ${route.routeName}} successfully created.`); + void vscode.window.showInformationMessage(`Route ${route.routeName} successfully created.`); CreateRouteViewLoader.panel.dispose(); CreateRouteViewLoader.panel = undefined; OpenShiftExplorer.getInstance().refresh(); @@ -138,11 +136,11 @@ export default class CreateRouteViewLoader { const flag = validateName(message.data.toString()); void CreateRouteViewLoader.panel.webview.postMessage({ action: 'validateRouteName', - data: { + data: JSON.stringify({ error: !flag ? false : true, helpText: !flag ? '' : flag, name: message.data.toString() - } + }) }); break; } @@ -150,22 +148,22 @@ export default class CreateRouteViewLoader { if (message.data.toString().trim() === '') { void CreateRouteViewLoader.panel.webview.postMessage({ action: 'validateHost', - data: { + data: JSON.stringify({ error: false, helpText: '', name: message.data.toString() - } + }) }); break; } const flag = validateURL(message, false); void CreateRouteViewLoader.panel.webview.postMessage({ action: 'validateHost', - data: { + data: JSON.stringify({ error: !flag.error ? false : true, helpText: flag.helpText, name: message.data.toString() - } + }) }); break; } @@ -173,22 +171,22 @@ export default class CreateRouteViewLoader { if (message.data.toString().trim() === '') { void CreateRouteViewLoader.panel.webview.postMessage({ action: 'validatePath', - data: { + data: JSON.stringify({ error: false, helpText: '', name: message.data.toString() - } + }) }); break; } const flag = validatePath(message.data.toString()); void CreateRouteViewLoader.panel.webview.postMessage({ action: 'validatePath', - data: { + data: JSON.stringify({ error: !flag ? false : true, helpText: !flag ? '' : flag, name: message.data.toString() - } + }) }); break; } From 8217c0a5f7050173ed2eda4f0ad05c2458a911db Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Wed, 22 May 2024 15:19:08 +0530 Subject: [PATCH 4/7] fixed lint issues --- src/webview/common-ext/utils.ts | 6 ++--- src/webview/common/route.ts | 2 +- src/webview/create-route/app/createForm.tsx | 17 +++++++------ .../create-route/createRouteViewLoader.ts | 24 +++++++------------ 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/webview/common-ext/utils.ts b/src/webview/common-ext/utils.ts index af1ed36a2..6aa9b50e3 100644 --- a/src/webview/common-ext/utils.ts +++ b/src/webview/common-ext/utils.ts @@ -119,11 +119,11 @@ 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', 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', - value, + JSON.parse(value) as unknown as string ); } if (!validationMessage) { validationMessage = NameValidator.lengthName('Should be between 2-63 characters', value, 0); } @@ -133,6 +133,6 @@ export function validateName(value: string): string | null { export function validatePath(value: string): string | null { return NameValidator.validatePath( 'Given path is not valid', - value, + JSON.parse(value) as unknown as string ); } diff --git a/src/webview/common/route.ts b/src/webview/common/route.ts index b448d5574..b6b2732ad 100644 --- a/src/webview/common/route.ts +++ b/src/webview/common/route.ts @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat; Inc. All rights reserved. + * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ diff --git a/src/webview/create-route/app/createForm.tsx b/src/webview/create-route/app/createForm.tsx index f3cd8e1f1..abb98215b 100644 --- a/src/webview/create-route/app/createForm.tsx +++ b/src/webview/create-route/app/createForm.tsx @@ -28,7 +28,6 @@ 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. */ @@ -259,30 +258,34 @@ export function CreateService() { setServiceKinds((_) => message.data); setPage((_) => 'PickServiceKind'); break; - case 'validateRouteName': - const routeData = JSON.parse(message.data); + 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 = JSON.parse(message.data); + + } + 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 = JSON.parse(message.data); + } + 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'); diff --git a/src/webview/create-route/createRouteViewLoader.ts b/src/webview/create-route/createRouteViewLoader.ts index 375b8fb37..fb5e92929 100644 --- a/src/webview/create-route/createRouteViewLoader.ts +++ b/src/webview/create-route/createRouteViewLoader.ts @@ -133,25 +133,25 @@ export default class CreateRouteViewLoader { break; } case 'validateRouteName': { - const flag = validateName(message.data.toString()); + const flag = validateName(JSON.stringify(message.data)); void CreateRouteViewLoader.panel.webview.postMessage({ action: 'validateRouteName', data: JSON.stringify({ error: !flag ? false : true, helpText: !flag ? '' : flag, - name: message.data.toString() + name: message.data }) }); break; } case 'validateHost': { - if (message.data.toString().trim() === '') { + if (JSON.stringify(message.data).trim() === '') { void CreateRouteViewLoader.panel.webview.postMessage({ action: 'validateHost', data: JSON.stringify({ error: false, helpText: '', - name: message.data.toString() + name: message.data }) }); break; @@ -162,30 +162,30 @@ export default class CreateRouteViewLoader { data: JSON.stringify({ error: !flag.error ? false : true, helpText: flag.helpText, - name: message.data.toString() + name: message.data }) }); break; } case 'validatePath': { - if (message.data.toString().trim() === '') { + if (JSON.stringify(message.data).trim() === '') { void CreateRouteViewLoader.panel.webview.postMessage({ action: 'validatePath', data: JSON.stringify({ error: false, helpText: '', - name: message.data.toString() + name: message.data }) }); break; } - const flag = validatePath(message.data.toString()); + 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.toString() + name: message.data }) }); break; @@ -195,9 +195,3 @@ export default class CreateRouteViewLoader { } } } - - - - - - From 1775812b4e34ad4d2d1cf2cc984fbfa69b61d741 Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Wed, 22 May 2024 20:56:49 +0530 Subject: [PATCH 5/7] fixed review comments --- src/oc/ocWrapper.ts | 3 +- src/webview/common-ext/utils.ts | 2 +- src/webview/common/createServiceTypes.ts | 4 -- src/webview/common/route.ts | 3 +- src/webview/create-route/app/createForm.tsx | 54 +++++++++++++++---- .../create-route/createRouteViewLoader.ts | 8 ++- 6 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/oc/ocWrapper.ts b/src/oc/ocWrapper.ts index 74a296c27..c9751b126 100644 --- a/src/oc/ocWrapper.ts +++ b/src/oc/ocWrapper.ts @@ -502,7 +502,7 @@ export class Oc { }); } - public async createRoute(routeName: string, serviceName: string, hostName: string, path: string, port: { number: string, name: string, protocol: string }, + 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) { @@ -516,6 +516,7 @@ export class Oc { 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) ]); } diff --git a/src/webview/common-ext/utils.ts b/src/webview/common-ext/utils.ts index 6aa9b50e3..d98200bff 100644 --- a/src/webview/common-ext/utils.ts +++ b/src/webview/common-ext/utils.ts @@ -126,7 +126,7 @@ export function validateName(value: string): string | null { JSON.parse(value) as unknown as string ); } - if (!validationMessage) { validationMessage = NameValidator.lengthName('Should be between 2-63 characters', value, 0); } + if (!validationMessage) { validationMessage = NameValidator.lengthName('Should be between 2-63 characters', JSON.parse(value) as unknown as string, 0); } return validationMessage; } diff --git a/src/webview/common/createServiceTypes.ts b/src/webview/common/createServiceTypes.ts index 744d30451..9bbf7db02 100644 --- a/src/webview/common/createServiceTypes.ts +++ b/src/webview/common/createServiceTypes.ts @@ -74,7 +74,6 @@ export type OwnerReference = { blockOwnerDeletion?: boolean; }; - export type ObjectMetadata = { annotations?: { [key: string]: string }; clusterName?: string; @@ -100,9 +99,6 @@ export type K8sResourceCommon = { metadata?: ObjectMetadata; }; - - - export type MatchExpression = { key: string; operator: 'Exists' | 'DoesNotExist' | 'In' | 'NotIn' | 'Equals' | 'NotEqual'; diff --git a/src/webview/common/route.ts b/src/webview/common/route.ts index b6b2732ad..fb294eecd 100644 --- a/src/webview/common/route.ts +++ b/src/webview/common/route.ts @@ -18,7 +18,8 @@ export type CreateRoute = { port: { number: string; name: string; - protocal: 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 index abb98215b..ce2e4d117 100644 --- a/src/webview/create-route/app/createForm.tsx +++ b/src/webview/create-route/app/createForm.tsx @@ -33,11 +33,16 @@ import { ErrorPage } from '../../common/errorPage'; */ function SelectService(props: { routeNameObj: RouteInputBoxText; + setRouteNameObj; hostNameObj: RouteInputBoxText; + setHostNameObj; pathObj: RouteInputBoxText; + setPathNameObj; serviceKinds: K8sResourceKind[]; selectedServiceKind: K8sResourceKind; setSelectedServiceKind; + ports: Port[], + setPorts; selectedPort: Port; setSelectedPort; }) { @@ -45,7 +50,6 @@ function SelectService(props: { const [isServiceKindTouched, setServiceKindTouched] = React.useState(false); const [isPortTouched, setPortTouched] = React.useState(false); const [isSecured, setSecured] = React.useState(false); - const [ports, setPorts] = React.useState([]); return (
+ props.setRouteNameObj((prevState: RouteInputBoxText) => ({ ...prevState, name: e.target.value })); + }} + /> ({ ...prevState, name: e.target.value })); }} /> ({ ...prevState, name: e.target.value })); }} /> Service @@ -117,7 +125,8 @@ function SelectService(props: { (serviceKind: K8sResourceKind) => serviceKind.metadata.name === e.target.value, ); props.setSelectedServiceKind((_) => newSelection); - setPorts((_) => newSelection.spec.ports); + props.setPorts((_) => newSelection.spec.ports); + props.setSelectedPort((_) => undefined); }} variant='outlined' placeholder='Select a service' @@ -146,7 +155,7 @@ function SelectService(props: { } }} onChange={(e) => { - const newSelection = ports.find( + const newSelection = props.ports.find( (port: Port) => port.port === e.target.value, ); props.setSelectedPort((_) => newSelection); @@ -156,7 +165,7 @@ function SelectService(props: { error={isServiceKindTouched && props.selectedServiceKind === undefined} required > - {ports.map((portObj: Port) => ( + {props.ports.map((portObj: Port) => ( {portObj.port} {portObj.targetPort} ({portObj.protocol}) @@ -179,7 +188,7 @@ function SelectService(props: { @@ -244,6 +256,7 @@ export function CreateService() { const [serviceKinds, setServiceKinds] = React.useState(undefined); const [selectedServiceKind, setSelectedServiceKind] = React.useState(undefined); + const [ports, setPorts] = React.useState([]); const [selectedPort, setSelectedPort] = React.useState(undefined); @@ -309,16 +322,22 @@ export function CreateService() { case 'Loading': return ; case 'Error': - return ; + pageElement = (); + break; case 'PickServiceKind': pageElement = ( ); @@ -329,7 +348,22 @@ export function CreateService() { return ( - {pageElement} + + {pageElement} + {error?.trim().length > 0 && + + + + } + ); } diff --git a/src/webview/create-route/createRouteViewLoader.ts b/src/webview/create-route/createRouteViewLoader.ts index fb5e92929..594aadf00 100644 --- a/src/webview/create-route/createRouteViewLoader.ts +++ b/src/webview/create-route/createRouteViewLoader.ts @@ -116,7 +116,8 @@ export default class CreateRouteViewLoader { const port = { name: route.port.name, number: route.port.number, - protocol: route.port.protocal + 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.`); @@ -132,6 +133,11 @@ export default class CreateRouteViewLoader { } break; } + case 'close': { + CreateRouteViewLoader.panel.dispose(); + CreateRouteViewLoader.panel = undefined; + break; + } case 'validateRouteName': { const flag = validateName(JSON.stringify(message.data)); void CreateRouteViewLoader.panel.webview.postMessage({ From c16114d6cb3209cd28340ed91c296c1ac5e498e6 Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Mon, 27 May 2024 12:42:15 +0530 Subject: [PATCH 6/7] fixed mentioned comments --- package-lock.json | 19 ------------------- package.json | 3 +-- src/explorer.ts | 4 ++-- src/openshift/nameValidator.ts | 4 ++-- 4 files changed, 5 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index f28ebd216..16f353601 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,7 +116,6 @@ "survey-core": "^1.10.3", "survey-react-ui": "^1.10.5", "typescript": "^5.4.5", - "valid-path": "^2.1.0", "vscode-extension-tester": "^8.1", "xterm-addon-fit": "^0.8.0", "xterm-addon-web-links": "^0.9.0", @@ -17851,15 +17850,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/valid-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/valid-path/-/valid-path-2.1.0.tgz", - "integrity": "sha512-hYanLM6kqE7zLl0oykV//2q3meRgYGOtS2lgChozuWjjghSqR8xwc3199bDGUFPwszk/MhvHhyVys8HdHU9buw==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -30195,15 +30185,6 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true }, - "valid-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/valid-path/-/valid-path-2.1.0.tgz", - "integrity": "sha512-hYanLM6kqE7zLl0oykV//2q3meRgYGOtS2lgChozuWjjghSqR8xwc3199bDGUFPwszk/MhvHhyVys8HdHU9buw==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 12a5e8cc6..9db826bf4 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,6 @@ "survey-core": "^1.10.3", "survey-react-ui": "^1.10.5", "typescript": "^5.4.5", - "valid-path": "^2.1.0", "vscode-extension-tester": "^8.1", "xterm-addon-fit": "^0.8.0", "xterm-addon-web-links": "^0.9.0", @@ -1634,7 +1633,7 @@ }, { "command": "openshift.route.create", - "when": "view == openshiftProjectExplorer && isLoggedIn && viewItem =~ /openshift.project.*/i && showCreateRoute", + "when": "view == openshiftProjectExplorer && isLoggedIn && viewItem =~ /openshift.project.*/i && isOpenshiftCluster && showCreateRoute", "group": "c2" }, { diff --git a/src/explorer.ts b/src/explorer.ts index 92759ebd9..2fcc36fe7 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -469,10 +469,10 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos name: 'build configs' }, } as OpenShiftObject; - result.push(pods, routes, + 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/openshift/nameValidator.ts b/src/openshift/nameValidator.ts index 3cea84915..7f0c7a41f 100644 --- a/src/openshift/nameValidator.ts +++ b/src/openshift/nameValidator.ts @@ -5,7 +5,6 @@ import * as path from 'path'; import validator from 'validator'; -import validPath = require('valid-path'); export function emptyName(message: string, value: string): string | null { return validator.isEmpty(value) ? message : null; @@ -24,7 +23,8 @@ export function validateMatches(message: string, value: string): string | null { } export function validatePath(message: string, value: string): string | null { - return validPath(value).valid ? null : message; + 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 { From 73e3c0aab29000ca34a58b9fc1d74b743e51db98 Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Mon, 27 May 2024 15:53:52 +0530 Subject: [PATCH 7/7] add separate validate name junction for json value --- src/webview/common-ext/utils.ts | 11 +++++++++++ src/webview/create-route/createRouteViewLoader.ts | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/webview/common-ext/utils.ts b/src/webview/common-ext/utils.ts index d98200bff..8b3e827e3 100644 --- a/src/webview/common-ext/utils.ts +++ b/src/webview/common-ext/utils.ts @@ -119,6 +119,17 @@ export function validateGitURL(event: Message): validateURLProps { } export function validateName(value: string): string | null { + 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 + ); + } + 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( diff --git a/src/webview/create-route/createRouteViewLoader.ts b/src/webview/create-route/createRouteViewLoader.ts index 594aadf00..85d03c60f 100644 --- a/src/webview/create-route/createRouteViewLoader.ts +++ b/src/webview/create-route/createRouteViewLoader.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { OpenShiftExplorer } from '../../explorer'; import { Oc } from '../../oc/ocWrapper'; import { ExtensionID } from '../../util/constants'; -import { loadWebviewHtml, validateName, validatePath, validateURL } from '../common-ext/utils'; +import { loadWebviewHtml, validateJSONValue, validatePath, validateURL } from '../common-ext/utils'; import { getServices as getService } from '../../openshift/serviceHelpers'; import type { CreateRoute } from '../common/route'; @@ -139,7 +139,7 @@ export default class CreateRouteViewLoader { break; } case 'validateRouteName': { - const flag = validateName(JSON.stringify(message.data)); + const flag = validateJSONValue(JSON.stringify(message.data)); void CreateRouteViewLoader.panel.webview.postMessage({ action: 'validateRouteName', data: JSON.stringify({