diff --git a/.eslintignore b/.eslintignore index c74715385..8d9be89c6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -18,6 +18,7 @@ src/webview/welcome src/webview/helm-chart src/webview/feedback src/webview/serverless-function +src/webview/serverless-manage-repository # vendored from https://github.com/IonicaBizau/git-url-parse, see https://github.com/IonicaBizau/git-url-parse/pull/159 src/util/gitParse.ts test/sandbox-registration diff --git a/build/esbuild.mjs b/build/esbuild.mjs index 6de044eb7..993617e1f 100644 --- a/build/esbuild.mjs +++ b/build/esbuild.mjs @@ -19,6 +19,7 @@ const webviews = [ 'welcome', 'feedback', 'serverless-function', + 'serverless-manage-repository', 'add-service-binding', ]; diff --git a/package.json b/package.json index c0590874a..df87d2070 100644 --- a/package.json +++ b/package.json @@ -784,6 +784,12 @@ "category": "OpenShift", "icon": "$(plus)" }, + { + "command": "openshift.Serverless.manageRepository", + "title": "Manage Knative Function Template Repositories", + "category": "OpenShift", + "icon": "$(gist)" + }, { "command": "openshift.Serverless.refresh", "title": "Refresh Serverless Function View", @@ -1299,6 +1305,11 @@ "when": "view == openshiftServerlessFunctionsView", "group": "navigation@1" }, + { + "command": "openshift.Serverless.manageRepository", + "when": "view == openshiftServerlessFunctionsView", + "group": "navigation@2" + }, { "command": "openshift.component.openCreateComponent", "when": "view == openshiftComponentsView", diff --git a/src/serverlessFunction/commands.ts b/src/serverlessFunction/commands.ts index 322bb2439..b39b579ad 100644 --- a/src/serverlessFunction/commands.ts +++ b/src/serverlessFunction/commands.ts @@ -117,19 +117,41 @@ export class ServerlessCommand { } static config(functionPath: string, mode: string, isAdd: boolean): CommandText { + const option = isAdd ? mode === 'git' ? 'set' : 'add' : 'remove'; const commandText = new CommandText('func', 'config', [ new CommandOption(mode), + new CommandOption(option), new CommandOption('-p', functionPath) ]); - if (isAdd) { - if (mode === 'git') { - commandText.addOption(new CommandOption('set')); - } else { - commandText.addOption(new CommandOption('add')); - } - } else { - commandText.addOption(new CommandOption('remove')); - } + return commandText; + } + + static addRepo(name: string, gitURL: string): CommandText { + const commandText = new CommandText('func', 'repository'); + commandText.addOption(new CommandOption('add')); + commandText.addOption(new CommandOption(name)); + commandText.addOption(new CommandOption(gitURL)); + return commandText; + } + + static deleteRepo(name: string): CommandText { + const commandText = new CommandText('func', 'repository'); + commandText.addOption(new CommandOption('remove')); + commandText.addOption(new CommandOption(name)); + return commandText; + } + + static list(): CommandText { + const commandText = new CommandText('func', 'repository'); + commandText.addOption(new CommandOption('list')); + return commandText; + } + + static renameRepo(oldName: string, newName: string): CommandText { + const commandText = new CommandText('func', 'repository'); + commandText.addOption(new CommandOption('rename')); + commandText.addOption(new CommandOption(oldName)); + commandText.addOption(new CommandOption(newName)); return commandText; } } diff --git a/src/serverlessFunction/manageRepository.ts b/src/serverlessFunction/manageRepository.ts new file mode 100644 index 000000000..6312fc481 --- /dev/null +++ b/src/serverlessFunction/manageRepository.ts @@ -0,0 +1,96 @@ +/*----------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import { OdoImpl } from '../odo'; +import sendTelemetry from '../telemetry'; +import { ServerlessCommand } from './commands'; + +export class ManageRepository { + + private static instance: ManageRepository; + + static getInstance(): ManageRepository { + if (!ManageRepository.instance) { + ManageRepository.instance = new ManageRepository(); + } + return ManageRepository.instance; + } + + public async deleteRepo(name: string): Promise { + await sendTelemetry('openshift.managerepo.delete', { + name + }); + const result = await OdoImpl.Instance.execute(ServerlessCommand.deleteRepo(name), '', false); + if (result.error) { + await sendTelemetry('openshift.managerepo.delete.error', { + error: result.error.message + }); + void vscode.window.showErrorMessage(result.error.message); + return false; + } + return true; + } + + public async renameRepo(oldName: string, newName: string): Promise { + await sendTelemetry('openshift.managerepo.rename', { + oldName, + newName + }); + const result = await OdoImpl.Instance.execute(ServerlessCommand.renameRepo(oldName, newName), '', false); + if (result.error) { + await sendTelemetry('openshift.managerepo.rename.error', { + error: result.error.message + }); + void vscode.window.showErrorMessage(result.error.message); + return false; + } + await sendTelemetry('openshift.managerepo.rename.success', { + message: `Repo ${newName} renamed successfully` + }); + return true; + } + + public async addRepo(name: string, url: string): Promise { + await sendTelemetry('openshift.managerepo.add', { + name, url + }); + const result = await OdoImpl.Instance.execute(ServerlessCommand.addRepo(name, url), '', false); + if (result.error) { + await sendTelemetry('openshift.managerepo.add.error', { + error: result.error.message + }); + void vscode.window.showErrorMessage(result.error.message); + return false; + } else if (result.stdout.length === 0 && result.stderr.length === 0) { + await sendTelemetry('openshift.managerepo.add.success', { + name, + message: 'Repo added successfully' + }); + void vscode.window.showInformationMessage(`Repository ${name} added successfully`); + return true; + } + await sendTelemetry('openshift.managerepo.add.error', { + error: result.stderr + }); + return false; + } + + public async list(): Promise { + await sendTelemetry('openshift.managerepo.list'); + const result = await OdoImpl.Instance.execute(ServerlessCommand.list(), '', false); + if (result.error) { + await sendTelemetry('openshift.managerepo.list.error', { + error: result.error.message + }); + void vscode.window.showErrorMessage(result.error.message); + return []; + } + await sendTelemetry('openshift.managerepo.list.success', { + repos: result.stdout.split('\n') + }); + return result.stdout.split('\n'); + } +} diff --git a/src/serverlessFunction/view.ts b/src/serverlessFunction/view.ts index f9f96d1aa..05fa68758 100644 --- a/src/serverlessFunction/view.ts +++ b/src/serverlessFunction/view.ts @@ -29,6 +29,7 @@ import { FunctionContextType, FunctionObject, FunctionStatus } from './types'; import ServerlessFunctionViewLoader from '../webview/serverless-function/serverlessFunctionLoader'; import { Functions } from './functions'; import { vsCommand } from '../vscommand'; +import ManageRepositoryViewLoader from '../webview/serverless-manage-repository/manageRepositoryLoader'; const kubeConfigFolder: string = path.join(Platform.getUserHomePath(), '.kube'); @@ -203,6 +204,11 @@ export class ServerlessFunctionView implements TreeDataProvider, D await ServerlessFunctionViewLoader.loadView('Serverless Function - Create'); } + @vsCommand('openshift.Serverless.manageRepository') + static async openManageRepository(): Promise { + await ManageRepositoryViewLoader.loadView('Manage Repository'); + } + @vsCommand('openshift.Serverless.refresh') static refresh(target?: ExplorerItem) { ServerlessFunctionView.getInstance().refresh(target); diff --git a/src/webview/common-ext/utils.ts b/src/webview/common-ext/utils.ts index 32ca226f9..df8bc5f26 100644 --- a/src/webview/common-ext/utils.ts +++ b/src/webview/common-ext/utils.ts @@ -6,7 +6,15 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import { Uri, WebviewPanel, extensions } from 'vscode'; +import OpenShiftItem from '../../openshift/openshiftItem'; import { ExtensionID } from '../../util/constants'; +import { gitUrlParse } from '../../util/gitParse'; +import { validateGitURLProps } from '../common/propertyTypes'; + +export type Message = { + action: string; + data: any; +}; export async function loadWebviewHtml(webviewName: string, webviewPanel: WebviewPanel, additionalInjections?: Map): Promise { @@ -28,7 +36,7 @@ export async function loadWebviewHtml(webviewName: string, webviewPanel: Webview style-src 'self' vscode-resource: 'unsafe-inline';">`; const htmlWithDefaultInjections = `${htmlString}` .replace('%PLATFORM%', process.platform) - .replace('%SCRIPT%',`${reactJavascriptUri}`) + .replace('%SCRIPT%', `${reactJavascriptUri}`) .replace('%BASE_URL%', `${reactJavascriptUri}`) .replace('%STYLE%', `${reactStylesheetUri}`) .replace('', meta); @@ -41,3 +49,62 @@ export async function loadWebviewHtml(webviewName: string, webviewPanel: Webview } return htmlWithAdditionalInjections; } + +function isGitURL(host: string): boolean { + return [ + 'github.com', + 'bitbucket.org', + 'gitlab.com', + 'git.sr.ht', + 'codeberg.org', + 'gitea.com', + ].includes(host); +} + +export function validateGitURL(event: Message): validateGitURLProps { + if (typeof event.data === 'string' && (event.data).trim().length === 0) { + return { + url: event.data, + error: true, + helpText: 'Please enter a Git URL.' + } as validateGitURLProps + } + try { + const parse = gitUrlParse(event.data); + const isGitRepo = isGitURL(parse.host); + if (!isGitRepo) { + throw 'Invalid Git URL'; + } + if (parse.organization !== '' && parse.name !== '') { + return { + url: event.data, + error: false, + helpText: 'The git repo URL is valid.' + } as validateGitURLProps + } + return { + url: event.data, + error: true, + helpText: 'URL is missing organization or repo name.' + } as validateGitURLProps + + } catch (e) { + return { + url: event.data, + error: true, + helpText: 'Invalid Git URL.' + } as validateGitURLProps + } +} + +export function validateName(value: string): string | null { + let validationMessage = OpenShiftItem.emptyName('Required', value.trim()); + if (!validationMessage) { + validationMessage = OpenShiftItem.validateMatches( + 'Only lower case alphabets and numeric characters or \'-\', start and ends with only alphabets', + value, + ); + } + if (!validationMessage) { validationMessage = OpenShiftItem.lengthName('Should be between 2-63 characters', value, 0); } + return validationMessage; +} diff --git a/src/webview/common/propertyTypes.ts b/src/webview/common/propertyTypes.ts index f64705780..8fd9d4f36 100644 --- a/src/webview/common/propertyTypes.ts +++ b/src/webview/common/propertyTypes.ts @@ -2,8 +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 { ChangeEvent } from 'react'; import { ComponentTypeDescription, Registry } from '../../odo/componentType'; import { StarterProject } from '../../odo/componentTypeDescription'; import { ChartResponse } from '../helm-chart/helmChartType'; @@ -84,3 +82,9 @@ export interface RunFunctionPageProps extends DefaultProps { skip: (stepCount: number) => void; onRunSubmit: (folderPath: Uri, build: boolean) => void; } + +export interface validateGitURLProps { + url: string; + error: boolean; + helpText: string; +} diff --git a/src/webview/common/utils.ts b/src/webview/common/utils.ts deleted file mode 100644 index c784e8477..000000000 --- a/src/webview/common/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*----------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE file in the project root for license information. - *-----------------------------------------------------------------------------------------------*/ - -import OpenShiftItem from '../../openshift/openshiftItem'; - -export function validateName(value: string): string | null { - let validationMessage = OpenShiftItem.emptyName('Required', value.trim()); - if (!validationMessage) - validationMessage = OpenShiftItem.validateMatches( - `Please use lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character`, - value, - ); - if (!validationMessage) - validationMessage = OpenShiftItem.lengthName('Should be between 2-63 characters', value, 0); - return validationMessage; -} diff --git a/src/webview/create-component/createComponentLoader.ts b/src/webview/create-component/createComponentLoader.ts index e8add06af..2a5268343 100644 --- a/src/webview/create-component/createComponentLoader.ts +++ b/src/webview/create-component/createComponentLoader.ts @@ -18,14 +18,13 @@ import { ComponentTypesView } from '../../registriesView'; import sendTelemetry from '../../telemetry'; import { ExtensionID } from '../../util/constants'; import { DevfileConverter } from '../../util/devfileConverter'; -import { gitUrlParse } from '../../util/gitParse'; import { selectWorkspaceFolder } from '../../util/workspace'; import { getDevfileRegistries, isValidProjectFolder, validateComponentName } from '../common-ext/createComponentHelpers'; -import { loadWebviewHtml } from '../common-ext/utils'; +import { loadWebviewHtml, validateGitURL } from '../common-ext/utils'; import { Devfile, DevfileRegistry, TemplateProjectIdentifier } from '../common/devfile'; interface CloneProcess { @@ -326,7 +325,14 @@ export default class CreateComponentLoader { * The panel requested to validate the git repository URL. */ case 'validateGitURL': { - validateGitURL(message); + const response = validateGitURL(message); + CreateComponentLoader.panel?.webview.postMessage({ + action: message.action, + data: { + isValid: !response.error, + helpText: response.helpText + } + }); break; } /** @@ -457,62 +463,6 @@ async function isDevfileExists(uri: vscode.Uri): Promise { } } -function validateGitURL(event: any) { - if (typeof event.data === 'string' && (event.data as string).trim().length === 0) { - CreateComponentLoader.panel?.webview.postMessage({ - action: event.action, - data: { - isValid: false, - helpText: 'Please enter a Git URL.', - }, - }); - } else { - try { - const parse = gitUrlParse(event.data); - const isGitRepo = isGitURL(parse.host); - if (!isGitRepo) { - throw 'Invalid Git URL'; - } - if (parse.organization !== '' && parse.name !== '') { - CreateComponentLoader.panel?.webview.postMessage({ - action: event.action, - data: { - isValid: true, - helpText: 'The git repo URL is valid.', - }, - }); - } else { - CreateComponentLoader.panel?.webview.postMessage({ - action: event.action, - data: { - isValid: false, - helpText: 'URL is missing organization or repo name.', - }, - }); - } - } catch (e) { - CreateComponentLoader.panel?.webview.postMessage({ - action: event.action, - data: { - isValid: false, - helpText: 'Invalid Git URL.', - }, - }); - } - } -} - -function isGitURL(host: string): boolean { - return [ - 'github.com', - 'bitbucket.org', - 'gitlab.com', - 'git.sr.ht', - 'codeberg.org', - 'gitea.com', - ].includes(host); -} - function clone(url: string, location: string, branch?: string): Promise { const gitExtension = vscode.extensions.getExtension('vscode.git').exports; const git = gitExtension.getAPI(1).git.path; diff --git a/src/webview/serverless-function/app/home.scss b/src/webview/serverless-function/app/home.scss index 0757f87db..ce665feda 100644 --- a/src/webview/serverless-function/app/home.scss +++ b/src/webview/serverless-function/app/home.scss @@ -30,7 +30,6 @@ button { width: auto; word-spacing: 2px; text-align: left; - color: var(--vscode-foreground); } .buttonStyle { @@ -88,7 +87,6 @@ button { .MuiTypography-root { font-family: var(--vscode-font-family) !important; - color: var(--vscode-settings-textInputForeground) !important; } .MuiFormHelperText-root { @@ -149,6 +147,18 @@ button { .MuiSvgIcon-root, .MuiStepLabel-label { + color: var(--vscode-button-foreground) !important; +} + +.successicon { + color: #198754 !important; +} + +.erroricon { + color: #ff3333 !important; +} + +.disabledicon { color: var(--vscode-settings-textInputForeground) !important; } @@ -161,7 +171,7 @@ button { } .Mui-disabled { - -webkit-text-fill-color: var(--vscode-titleBar-inactiveForeground) !important; + -webkit-text-fill-color: var(--vscode-button-foreground) !important; } .Mui-active { @@ -176,3 +186,20 @@ button { li:hover { background-color: var(--vscode-button-hoverBackground) !important; } + +.MuiDialog-paper { + border: 1px groove var(--vscode-activityBar-activeBorder) !important; + border-radius: 1rem !important; + margin: auto !important; + background-color: #101418 !important; + color: var(--vscode-settings-textInputForeground) !important +} + +.MuiTableCell-body { + color: var(--vscode-button-foreground) !important +} + +.MuiTablePagination-toolbar { + background-color: var(--vscode-button-secondaryHoverBackground) !important; + color: var(--vscode-settings-textInputForeground) !important +} diff --git a/src/webview/serverless-function/serverlessFunctionLoader.ts b/src/webview/serverless-function/serverlessFunctionLoader.ts index 9457f78f9..af388433f 100644 --- a/src/webview/serverless-function/serverlessFunctionLoader.ts +++ b/src/webview/serverless-function/serverlessFunctionLoader.ts @@ -11,8 +11,7 @@ import { serverlessInstance } from '../../serverlessFunction/functionImpl'; import { ExtensionID } from '../../util/constants'; import { Progress } from '../../util/progress'; import { selectWorkspaceFolder, selectWorkspaceFolders } from '../../util/workspace'; -import { loadWebviewHtml } from '../common-ext/utils'; -import { validateName } from '../common/utils'; +import { loadWebviewHtml, validateName } from '../common-ext/utils'; import { InvokeFunction } from '../../serverlessFunction/types'; export interface ServiceBindingFormResponse { diff --git a/src/webview/serverless-manage-repository/app/addRepository.tsx b/src/webview/serverless-manage-repository/app/addRepository.tsx new file mode 100644 index 000000000..c67129dc1 --- /dev/null +++ b/src/webview/serverless-manage-repository/app/addRepository.tsx @@ -0,0 +1,163 @@ +/*----------------------------------------------------------------------------------------------- + * 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 React from 'react'; +import { Button, Stack, TextField } from '@mui/material'; +import { DefaultProps } from '../../common/propertyTypes'; +import './home.scss'; +import { VSCodeMessage } from './vsCodeMessage'; + +export class AddRepository extends React.Component { + + constructor(props: DefaultProps | Readonly) { + super(props); + this.state = { + input: { + name: '', + error: false, + helpText: '' + }, + repo: { + url: '', + error: false, + helpText: '' + } + } + } + + componentDidMount(): void { + VSCodeMessage.onMessage((message) => { + if (message.data.action === 'validateGitURL') { + this.setState({ + repo: { + url: message.data.url, + error: message.data.error, + helpText: message.data.helpText + } + }) + } else if (message.data.action === 'validateName') { + this.setState({ + input: { + name: message.data.name, + error: message.data.error, + helpText: message.data.helpText + } + }) + } + }); + } + + handleButtonDisable(): boolean { + return this.state.input.name?.length === 0 || this.state.input.error + || this.state.repo.url?.length === 0 || this.state.repo.error + } + + validateGitURL = (value: string): void => { + VSCodeMessage.postMessage({ + action: `validateGitURL`, + data: value + }) + } + + validateName = (value: string): void => { + VSCodeMessage.postMessage({ + action: `validateName`, + data: value + }) + } + + addRepo = (): void => { + VSCodeMessage.postMessage({ + action: `addRepo`, + data: { + name: this.state.input.name, + url: this.state.repo.url + } + }) + } + + + render(): React.ReactNode { + const { input, repo } = this.state; + return ( + + + + this.validateName(e.target.value)} + id='git-name' + placeholder='Provide repository name' + sx={{ + input: { + color: 'var(--vscode-settings-textInputForeground)', + height: '7px !important', + } + }} + helperText={input.helpText} /> + + + + this.validateGitURL(e.target.value)} + id='git-url' + placeholder='Provide git URL' + sx={{ + input: { + color: 'var(--vscode-settings-textInputForeground)', + height: '7px !important', + } + }} + helperText={repo.helpText} /> + + + + + + ) + } +} diff --git a/src/webview/serverless-manage-repository/app/home.scss b/src/webview/serverless-manage-repository/app/home.scss new file mode 100644 index 000000000..ce665feda --- /dev/null +++ b/src/webview/serverless-manage-repository/app/home.scss @@ -0,0 +1,205 @@ +body { + &.vscode-light { + background-color: var(--color-background--darken-05); + } +} + +button { + padding: 0; +} + +.margin { + margin: 3rem; +} + +.mainContainer, +.formContainer, +.form { + display: flex; + flex-direction: column; + font-family: var(--vscode-font-family) !important; +} + +.title { + width: 100%; + margin-bottom: 1rem; +} + +.subTitle { + margin-bottom: 2rem; + width: auto; + word-spacing: 2px; + text-align: left; +} + +.buttonStyle { + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + color: var(--vscode-button-foreground); + width: 3rem; + height: 2.5rem !important; + &:hover { + background-color: '#BE0000' !important; + } + + text-transform: none; +} + +.MuiButton-root.Mui-disabled { + -webkit-text-fill-color: white !important; +} + +.labelStyle { + text-align: center; + text-transform: none; + height: 2.5rem !important; + background-color: var(--vscode-button-background) !important; + border: 1px solid var(--vscode-settings-textInputForeground); +} + +.strategyContainer { + display: flex; + flex-direction: row; + border-top: 3px solid; + max-width: 55rem; + margin-top: 2rem; + padding-top: 0.5rem; + padding-left: 1rem; + min-height: 2rem; +} + +.cardContainer { + display: flex; + flex-direction: column; + margin-top: 1rem; +} + +.strategySuccess { + border-color: green; + background-color: darkseagreen !important; +} + +.strategyWarning { + border-color: orange; + background-color: burlywood !important; +} + +.MuiTypography-root { + font-family: var(--vscode-font-family) !important; +} + +.MuiFormHelperText-root { + color: var(--vscode-foreground) !important; + font-family: var(--vscode-font-family) !important; + margin-left: 0px !important; +} + +.Mui-error { + color: #EE0000 !important; +} + +.MuiSelect-select, +.MuiAutocomplete-inputRoot { + color: var(--vscode-dropdown-foreground) !important; +} + +.MuiMenu-paper { + background-color: var(--vscode-dropdown-background) !important; + color: var(--vscode-dropdown-foreground) !important; +} + +.MuiInputLabel-root, +.MuiFormLabel-root { + background-color: transparent; + color: var(--vscode-settings-textInputForeground) !important; +} + +.MuiAutocomplete-inputRoot { + padding: 0 !important; + padding-left: 5px !important; + height: 2.5rem !important; +} + +.MuiMenuItem-root { + background-color: var(--vscode-dropdown-background) !important; + color: var(--vscode-dropdown-foreground) !important; +} + +.MuiMenuItem-root:hover { + background-color: var(--vscode-button-hoverBackground) !important; +} + +.MuiFormControl-root { + border: none; + &:hover { + border: none; + } +} + +.MuiOutlinedInput-notchedOutline { + border-color: var(--vscode-settings-textInputForeground) !important; +} + +.MuiFormControl-fullWidth { + max-width: 100% !important; +} + +.MuiSvgIcon-root, +.MuiStepLabel-label { + color: var(--vscode-button-foreground) !important; +} + +.successicon { + color: #198754 !important; +} + +.erroricon { + color: #ff3333 !important; +} + +.disabledicon { + color: var(--vscode-settings-textInputForeground) !important; +} + +.MuiCircularProgress-svg { + color: #EE0000 !important; +} + +.MuiOutlinedInput-root.Mui-disabled { + border: 0px solid var(--vscode-tab-unfocusedActiveBorder) !important; +} + +.Mui-disabled { + -webkit-text-fill-color: var(--vscode-button-foreground) !important; +} + +.Mui-active { + color: var(--vscode-button-hoverBackground) !important; +} + +.MuiAutocomplete-noOptions { + background-color: var(--vscode-settings-textInputBackground); + color: var(--vscode-settings-textInputForeground) !important; +} + +li:hover { + background-color: var(--vscode-button-hoverBackground) !important; +} + +.MuiDialog-paper { + border: 1px groove var(--vscode-activityBar-activeBorder) !important; + border-radius: 1rem !important; + margin: auto !important; + background-color: #101418 !important; + color: var(--vscode-settings-textInputForeground) !important +} + +.MuiTableCell-body { + color: var(--vscode-button-foreground) !important +} + +.MuiTablePagination-toolbar { + background-color: var(--vscode-button-secondaryHoverBackground) !important; + color: var(--vscode-settings-textInputForeground) !important +} diff --git a/src/webview/serverless-manage-repository/app/home.tsx b/src/webview/serverless-manage-repository/app/home.tsx new file mode 100644 index 000000000..a38b17ee4 --- /dev/null +++ b/src/webview/serverless-manage-repository/app/home.tsx @@ -0,0 +1,26 @@ +/*----------------------------------------------------------------------------------------------- + * 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 React from 'react'; +import { DefaultProps } from '../../common/propertyTypes'; +import './home.scss'; +import { ShowRepositories } from './showRepositories'; +import { VSCodeMessage } from './vsCodeMessage'; + +export class ManageRepository extends React.Component { + + constructor(props: DefaultProps | Readonly) { + super(props); + VSCodeMessage.postMessage({ + action: `getRepositoryList` + }); + } + + render(): React.ReactNode { + return ( + + ) + } +} diff --git a/src/webview/serverless-manage-repository/app/index.html b/src/webview/serverless-manage-repository/app/index.html new file mode 100644 index 000000000..cb9b0ca63 --- /dev/null +++ b/src/webview/serverless-manage-repository/app/index.html @@ -0,0 +1,52 @@ + + + + + + + Serverless Function + + + + +
+ + + + diff --git a/src/webview/serverless-manage-repository/app/index.tsx b/src/webview/serverless-manage-repository/app/index.tsx new file mode 100644 index 000000000..769c62ea9 --- /dev/null +++ b/src/webview/serverless-manage-repository/app/index.tsx @@ -0,0 +1,12 @@ +/*----------------------------------------------------------------------------------------------- + * 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 { ManageRepository } from './home'; + +ReactDOM.render( + , + document.getElementById('root'), +); diff --git a/src/webview/serverless-manage-repository/app/showRepositories.tsx b/src/webview/serverless-manage-repository/app/showRepositories.tsx new file mode 100644 index 000000000..908bee332 --- /dev/null +++ b/src/webview/serverless-manage-repository/app/showRepositories.tsx @@ -0,0 +1,357 @@ +/*----------------------------------------------------------------------------------------------- + * 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 React from 'react'; +import { Add, Cancel, Delete, Done, Edit } from '@mui/icons-material'; +import { VSCodeMessage } from './vsCodeMessage'; +import { DefaultProps } from '../../common/propertyTypes'; +import { Typography, Stack, Button, Dialog, DialogActions, DialogContent, TextField, TableCell, TableRow, Table, TableContainer, TableHead, styled, tableCellClasses, TableBody, Box, TablePagination, IconButton, Container, CircularProgress, Tooltip } from '@mui/material'; +import { AddRepository } from './addRepository'; + +export class ShowRepositories extends React.Component { + constructor(props: DefaultProps | Readonly) { + super(props); + this.state = { + repositories: [], + openAddDialog: false, + openEditDialog: false, + openDeleteDialog: false, + openedRepo: '', + newRepoInput: { + name: '', + error: false, + helpText: '' + }, + page: 0, + rowsPerPage: 10 + } + } + + componentDidMount(): void { + VSCodeMessage.onMessage((message) => { + if (message.data.action === 'getRepositoryList') { + this.setState({ + repositories: message.data.repositories + }); + } else if (message.data.action === 'validateNewName') { + this.setState({ + newRepoInput: { + name: message.data.name, + error: message.data.error, + helpText: message.data.helpText + } + }) + } else if (message.data.action === 'addRepo' && message.data.status) { + this.setState({ + openAddDialog: false + }); + VSCodeMessage.postMessage({ + action: `getRepositoryList` + }); + } + }); + } + + handleDialog = (repoName: string, isEdit = true): void => { + if (!repoName) { + this.setState({ + openAddDialog: !this.state.openAddDialog + }) + } else { + this.setState({ + openedRepo: repoName, + newRepoInput: { + name: repoName, + error: false, + helpText: '' + }, + openDeleteDialog: isEdit ? false : !this.state.openDeleteDialog, + openEditDialog: isEdit ? !this.state.openEditDialog : false + }); + } + } + + delete = (repo: string): void => { + this.CloseDialog(); + VSCodeMessage.postMessage({ + action: `deleteRepo`, + data: { + name: repo + } + }); + } + + CloseDialog = (): void => { + this.setState({ + openAddDialog: false, + openEditDialog: false, + openDeleteDialog: false, + openedRepo: '', + newRepoInput: { + name: '', + error: false, + helpText: '' + } + }); + } + + setRepoName = (value: string): boolean => { + this.setState({ + openEditDialog: true, + newRepoInput: { + name: value, + error: false, + helpText: '' + } + }); + return this.state.openEditDialog; + } + + validateName = (value: string): void => { + VSCodeMessage.postMessage({ + action: `validateNewName`, + data: value + }); + } + + handleDisable = (): boolean => { + return this.state.newRepoInput.name.length === 0 || this.state.newRepoInput.error + } + + rename(oldName: string, newName: string): void { + this.CloseDialog(); + VSCodeMessage.postMessage({ + action: `renameRepo`, + data: { + oldName, + newName + } + }); + } + + StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: 'var(--vscode-button-background)', + color: 'var(--vscode-button-foreground)' + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + }, + })); + + StyledTableRow = styled(TableRow)(({ }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: 'var(--vscode-button-secondaryBackground)' + }, + + '&:nth-of-type(even)': { + backgroundColor: 'var(--vscode-button-secondaryHoverBackground)' + } + })); + + handleChangeRowsPerPage = (event: React.ChangeEvent) => { + this.setState({ + page: 0, + rowsPerPage: parseInt(event.target.value, 10) + }); + }; + + handleChangePage = (_event: unknown, newPage: number) => { + this.setState({ + page: newPage + }); + }; + + render(): React.ReactNode { + const { newRepoInput, openedRepo, openAddDialog, openDeleteDialog, openEditDialog, page, repositories, rowsPerPage } = this.state; + return ( +
+
+ Manage Repositories +
+
+ Manage template repositories installed on disk at the default location (~/.config/func/repositories) Once added, a template from the repository can be used when creating a new function. +
+ + + <> + { + repositories.length > 0 ? + <> + + + + + + Repository Name + Actions + + + + {repositories.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((repo) => ( + + +
+ <> + { + repo.length > 24 ? + {repo} + : {repo} + } + +
+
+ + + + this.handleDialog(repo)} + > + + + + + this.handleDialog(repo, false)} + > + + + + + +
+ ))} +
+
+
+ { + repositories.length > 10 && + + } + +
+ + + + + + + : + + } + +
+
+ { + openedRepo.length > 0 && + this.CloseDialog()}> + + + {`Are you sure want to delete the repository '${openedRepo}' ?`} + + + + + + + + } + { + openedRepo.length > 0 && + this.CloseDialog()}> + + + + this.validateName(e.target.value)} + id='repo-new-name' + sx={{ + input: { + color: 'var(--vscode-settings-textInputForeground)', + height: '7px !important', + } + }} + helperText={newRepoInput.helpText} /> + + + + + + + + } + this.CloseDialog()}> + + + + +
+ ) + } +} diff --git a/src/webview/serverless-manage-repository/app/tsconfig.json b/src/webview/serverless-manage-repository/app/tsconfig.json new file mode 100644 index 000000000..9d75542c3 --- /dev/null +++ b/src/webview/serverless-manage-repository/app/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "module": "esnext", + "moduleResolution": "node", + "target": "es6", + "outDir": "configViewer", + "lib": [ + "es6", + "dom" + ], + "jsx": "react", + "sourceMap": true, + "noUnusedLocals": true, + // "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "experimentalDecorators": true, + "typeRoots": [ + "../../../../node_modules/@types", + "../../@types" + ], + "baseUrl": ".", + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/src/webview/serverless-manage-repository/app/vsCodeMessage.ts b/src/webview/serverless-manage-repository/app/vsCodeMessage.ts new file mode 100644 index 000000000..aac1a4b7a --- /dev/null +++ b/src/webview/serverless-manage-repository/app/vsCodeMessage.ts @@ -0,0 +1,37 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +declare const acquireVsCodeApi: Function; + +interface VSCodeApi { + getState: () => any; + setState: (newState: any) => any; + postMessage: (message: any) => void; +} + +class VSCodeWrapper { + private readonly vscodeApi: VSCodeApi = acquireVsCodeApi(); + + /** + * Send message to the extension framework. + * @param message + */ + public postMessage(message: any): void { + this.vscodeApi.postMessage(message); + } + + /** + * Add listener for messages from extension framework. + * @param callback called when the extension sends a message + * @returns function to clean up the message eventListener. + */ + public onMessage(callback: (message: any) => void): () => void { + window.addEventListener('message', callback); + return () => window.removeEventListener('message', callback); + } +} + +// Singleton to prevent multiple fetches of VsCodeAPI. +export const VSCodeMessage: VSCodeWrapper = new VSCodeWrapper(); diff --git a/src/webview/serverless-manage-repository/manageRepositoryLoader.ts b/src/webview/serverless-manage-repository/manageRepositoryLoader.ts new file mode 100644 index 000000000..4d181ac36 --- /dev/null +++ b/src/webview/serverless-manage-repository/manageRepositoryLoader.ts @@ -0,0 +1,139 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ +import { ChildProcess } from 'child_process'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { ManageRepository } from '../../serverlessFunction/manageRepository'; +import { ExtensionID } from '../../util/constants'; +import { Progress } from '../../util/progress'; +import { loadWebviewHtml, Message, validateGitURL, validateName } from '../common-ext/utils'; + +export default class ManageRepositoryViewLoader { + + static panel: vscode.WebviewPanel; + + public static processMap: Map = new Map(); + + private static get extensionPath(): string { + return vscode.extensions.getExtension(ExtensionID).extensionPath; + } + + /** + * Returns a webview panel with the "Add Service Binding" UI, + * or if there is an existing view for the given contextPath, focuses that view and returns null. + * + * @param contextPath the path to the component that's being binded to a service + * @param availableServices the list of all bindable services on the cluster + * @param listenerFactory the listener function to receive and process messages from the webview + * @return the webview as a promise + */ + static async loadView( + title: string + ): Promise { + if (ManageRepositoryViewLoader.panel) { + ManageRepositoryViewLoader.panel.reveal(); + return; + } + const localResourceRoot = vscode.Uri.file( + path.join(ManageRepositoryViewLoader.extensionPath, 'out', 'serverlessManageRepositoryViewer'), + ); + + let panel = vscode.window.createWebviewPanel('manageRepositoryView', title, vscode.ViewColumn.One, { + enableScripts: true, + localResourceRoots: [localResourceRoot], + retainContextWhenHidden: true, + }); + + const messageHandlerDisposable = panel.webview.onDidReceiveMessage( + ManageRepositoryViewLoader.messageHandler, + ); + + panel.onDidDispose(() => { + messageHandlerDisposable.dispose(); + ManageRepositoryViewLoader.panel = undefined; + }); + + panel.iconPath = vscode.Uri.file( + path.join(ManageRepositoryViewLoader.extensionPath, 'images/context/cluster-node.png'), + ); + + panel.webview.html = await loadWebviewHtml('serverlessManageRepositoryViewer', panel); + ManageRepositoryViewLoader.panel = panel; + + return panel; + } + + static async messageHandler(message: Message) { + const action = message.action; + switch (action) { + case 'validateGitURL': + const data = validateGitURL(message); + ManageRepositoryViewLoader.panel?.webview.postMessage({ + action, + url: data.url, + error: data.error, + helpText: data.helpText + }); + break; + case 'validateName': + case 'validateNewName': + const flag = validateName(message.data); + const repoList = await ManageRepository.getInstance().list(); + if (repoList.includes(message.data)) { + ManageRepositoryViewLoader.panel?.webview.postMessage({ + action: action, + name: message.data, + error: true, + helpText: `Repository ${message.data} already exists` + }); + } else { + ManageRepositoryViewLoader.panel?.webview.postMessage({ + action: action, + name: message.data, + error: !flag ? false : true, + helpText: !flag ? '' : flag + }); + } + break; + case 'addRepo': + let addRepoStatus: boolean; + await Progress.execFunctionWithProgress(`Adding repository ${message.data.name}`, async () => { + addRepoStatus = await ManageRepository.getInstance().addRepo(message.data.name, message.data.url); + }); + ManageRepositoryViewLoader.panel?.webview.postMessage({ + action: action, + status: addRepoStatus + }); + break; + case 'getRepositoryList': + const repositories = await ManageRepository.getInstance().list(); + ManageRepositoryViewLoader.panel?.webview.postMessage({ + action: action, + repositories: repositories + }); + break; + case 'deleteRepo': + const status = await ManageRepository.getInstance().deleteRepo(message.data.name); + if (status) { + const repositories = await ManageRepository.getInstance().list(); + ManageRepositoryViewLoader.panel?.webview.postMessage({ + action: 'getRepositoryList', + repositories: repositories + }); + } + break; + case 'renameRepo': + const renameRepoStatus = await ManageRepository.getInstance().renameRepo(message.data.oldName, message.data.newName); + if (renameRepoStatus) { + const repositories = await ManageRepository.getInstance().list(); + ManageRepositoryViewLoader.panel?.webview.postMessage({ + action: 'getRepositoryList', + repositories: repositories + }); + } + break; + } + } +} diff --git a/tsconfig.json b/tsconfig.json index bc97936fb..bd25b22f5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,6 +38,7 @@ "src/webview/helm-chart/app", "src/webview/feedback/app", "src/webview/serverless-function/app", + "src/webview/serverless-manage-repository/app", "src/webview/common" ] }