From 841b35a4893b75c2b5bfb724bf6ca5091a5c72e6 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 21 Nov 2024 18:35:24 -0500 Subject: [PATCH 01/19] console: add rpcn create secret page --- .../rp-connect/secrets/Secrets.Create.tsx | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx new file mode 100644 index 000000000..9c6b24c43 --- /dev/null +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx @@ -0,0 +1,139 @@ +import {Button, ButtonGroup, createStandaloneToast, Flex, FormField, Input} from '@redpanda-data/ui'; +import {PageComponent, PageInitHelper} from '../../Page'; +import {observer} from 'mobx-react'; +import {appGlobal} from '../../../../state/appGlobal'; +import {pipelinesApi, rpcnSecretManagerApi} from '../../../../state/backendApi'; +import PageContent from '../../../misc/PageContent'; +import {action, makeObservable, observable} from 'mobx'; +import {DefaultSkeleton} from '../../../../utils/tsxUtils'; +import {formatPipelineError} from '../errors'; +import {CreateSecretRequest, Scope} from '../../../../protogen/redpanda/api/dataplane/v1alpha2/secret_pb'; + +const {ToastContainer, toast} = createStandaloneToast(); + +@observer +class RpConnectSecretCreate extends PageComponent { + + @observable id = ''; + @observable secret = ''; + @observable isCreating = false; + + constructor(p: any) { + super(p); + makeObservable(this, undefined, {autoBind: true}); + } + + initPage(p: PageInitHelper) { + p.title = 'Create Secret'; + p.addBreadcrumb('Redpanda Connect Secret Manager', '/rp-connect/secret/create'); + p.addBreadcrumb('Create Secret', ''); + + this.refreshData(true); + appGlobal.onRefresh = () => this.refreshData(true); + } + + refreshData(_force: boolean) { + rpcnSecretManagerApi.refreshSecrets(_force); + } + + cancel() { + this.secret = ''; + this.id = ''; + appGlobal.history.push('/connect-clusters/redpanda-connect-secret'); + } + + async createSecret() { + this.isCreating = true; + + //create function given string return base64 encoded Uint8Array + function base64Encode(str: string): Uint8Array { + const encodedString = btoa(str); + const charList = encodedString.split('').map(char => char.charCodeAt(0)); + return new Uint8Array(charList); + } + + + rpcnSecretManagerApi.create(new CreateSecretRequest({ + id: this.id, + secretData: base64Encode(this.secret), + scopes: [Scope.REDPANDA_CONNECT] + })) + .then(async () => { + toast({ + status: 'success', duration: 4000, isClosable: false, + title: 'Pipeline created' + }); + await pipelinesApi.refreshPipelines(true); + appGlobal.history.push('/connect-clusters/redpanda-connect-secret'); + }) + .catch(err => { + toast({ + status: 'error', duration: null, isClosable: true, + title: 'Failed to create pipeline', + description: formatPipelineError(err), + }) + }) + .finally(() => { + this.isCreating = false; + }); + } + + render() { + if (!rpcnSecretManagerApi.secrets) return DefaultSkeleton; + + const alreadyExists = (rpcnSecretManagerApi.secrets || []).any(x => x.id == this.id); + const isIdEmpty = this.id.trim().length == 0; + const isSecretEmpty = this.secret.trim().length == 0; + + return ( + + + + + + this.id = x.target.value.toUpperCase()} + width={500} + disabled={this.isCreating} + /> + + + + + + this.secret = x.target.value} + width={500} + type="password" + disabled={this.isCreating} + /> + + + + + + + + + + ); + + } +} + +export default RpConnectSecretCreate; From 7cabae5c1819b5877a110c3fadfe728d662ad8e5 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 21 Nov 2024 18:35:33 -0500 Subject: [PATCH 02/19] console: add rpcn list secret page --- .../pages/rp-connect/secrets/Secrets.List.tsx | 181 ++++++++++++++++++ frontend/src/state/ui.ts | 4 + 2 files changed, 185 insertions(+) create mode 100644 frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx new file mode 100644 index 000000000..37269d63e --- /dev/null +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx @@ -0,0 +1,181 @@ +import React from 'react'; +import {PageComponent, PageInitHelper} from '../../Page'; +import {observer} from 'mobx-react'; +import {appGlobal} from '../../../../state/appGlobal'; +import {rpcnSecretManagerApi} from '../../../../state/backendApi'; +import {Features} from '../../../../state/supportedFeatures'; +import {Box, Button, ButtonGroup, Code, ConfirmItemDeleteModal, createStandaloneToast, DataTable, Flex, Image, SearchField, Text} from '@redpanda-data/ui'; +import Section from '../../../misc/Section'; +import PageContent from '../../../misc/PageContent'; +import {uiSettings} from '../../../../state/ui'; +import {Link} from 'react-router-dom'; +import {encodeURIComponentPercents} from '../../../../utils/utils'; +import {PencilIcon, TrashIcon} from '@heroicons/react/outline'; +import EmptyConnectors from '../../../../assets/redpanda/EmptyConnectors.svg'; +import {DeleteSecretRequest, Secret} from '../../../../protogen/redpanda/api/dataplane/v1alpha2/secret_pb'; + +const {ToastContainer, toast} = createStandaloneToast(); + +const CreateSecretButton = () => { + return ( + + ) +} + +const EmptyPlaceholder = () => { + return + + You have no Redpanda Connect secrets. + + +}; + +@observer +class RpConnectSecretsList extends PageComponent { + + initPage(p: PageInitHelper) { + p.addBreadcrumb('Redpanda Connect Secret Manager', '/rp-connect/secrets'); + this.refreshData(true); + appGlobal.onRefresh = () => this.refreshData(true); + } + + refreshData(force: boolean) { + if (!Features.pipelinesApi) return; + + rpcnSecretManagerApi.refreshSecrets(force) + .catch((err) => { + if (String(err).includes('404')) { + // Hacky special handling for OSS version, it is possible for the /endpoints request to not complete in time for this to render + // so in this case there would be an error shown because we were too fast (with rendering, or the req was too slow) + // We don't want to show an error in that case + return; + } + + if (Features.pipelinesApi) { + toast({ + status: 'error', duration: null, isClosable: true, + title: 'Failed to load pipelines', + description: String(err), + }); + } + }) + } + + async deleteSecret(id: string) { + await rpcnSecretManagerApi.delete(new DeleteSecretRequest({ + id + })) + this.refreshData(true); + } + + render() { + + const filteredSecrets = (rpcnSecretManagerApi.secrets ?? []) + .filter(u => { + const filter = uiSettings.rpncSecretList.quickSearch; + if (!filter) return true; + try { + const quickSearchRegExp = new RegExp(filter, 'i'); + if (u.id.match(quickSearchRegExp)) + return true; + return false; + } catch { + return false; + } + }); + + return ( + +
+ + + + uiSettings.rpncSecretList.quickSearch = x} + placeholderText="Enter search term / regex..." + /> + + + + {(rpcnSecretManagerApi.secrets ?? []).length == 0 + ? + : + data={filteredSecrets} + pagination + defaultPageSize={10} + sorting + columns={[ + { + header: 'Secret name', + cell: ({row: {original}}) => ( + + {original.id} + + ), + size: 200, + }, + { + header: 'Secret notation', + cell: ({row: {original}}) => ( + + {`$(secrets.${original.id})`} + + ), + size: 400 + }, + // let use this on next phase + // { + // header: 'Pipelines', + // cell: (_) => ( + // TODO + // ), + // size: 400, + // }, + { + header: '', + id: 'actions', + cell: ({row: {original: r}}) => + + + + + } itemType={'Secret'} + onConfirm={ + async (dismiss) => { + await this.deleteSecret(r.id) + dismiss(); + } + }> + Deleting this secret may disrupt the functionality of pipelines that depend on it. Are you sure you want to delete the secret {r.id}? + + + + , + size: 10 + }, + ]} + emptyText="" + /> + } + +
+
+ ) + } +} + +export default RpConnectSecretsList; diff --git a/frontend/src/state/ui.ts b/frontend/src/state/ui.ts index 2c05e1fe5..dffff6f67 100644 --- a/frontend/src/state/ui.ts +++ b/frontend/src/state/ui.ts @@ -234,6 +234,10 @@ const defaultUiSettings = { quickSearch: '' }, + rpncSecretList: { + quickSearch: '' + }, + pipelinesDetails: { logsQuickSearch: '', sorting: [] as SortingState, From bf4f64f6fb7b23278463136ad33ef7611ba2ee53 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 21 Nov 2024 18:35:39 -0500 Subject: [PATCH 03/19] console: add rpcn update secret page --- .../rp-connect/secrets/Secrets.Update.tsx | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx new file mode 100644 index 000000000..23948007c --- /dev/null +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx @@ -0,0 +1,132 @@ +import {Button, ButtonGroup, createStandaloneToast, Flex, FormField, Input} from '@redpanda-data/ui'; +import {PageComponent, PageInitHelper} from '../../Page'; +import {observer} from 'mobx-react'; +import {appGlobal} from '../../../../state/appGlobal'; +import {pipelinesApi, rpcnSecretManagerApi} from '../../../../state/backendApi'; +import PageContent from '../../../misc/PageContent'; +import {action, makeObservable, observable} from 'mobx'; +import {DefaultSkeleton} from '../../../../utils/tsxUtils'; +import {formatPipelineError} from '../errors'; +import {Scope, UpdateSecretRequest} from '../../../../protogen/redpanda/api/dataplane/v1alpha2/secret_pb'; + +const {ToastContainer, toast} = createStandaloneToast(); + +@observer +class RpConnectSecretUpdate extends PageComponent<{ secretId: string }> { + @observable secret = ''; + @observable isUpdating = false; + + constructor(p: any) { + super(p); + makeObservable(this, undefined, {autoBind: true}); + } + + initPage(p: PageInitHelper) { + p.title = 'Update Secret'; + p.addBreadcrumb('Redpanda Connect Secret Manager', '/rp-connect/secret/update'); + p.addBreadcrumb('Update Secret', ''); + + this.refreshData(true); + appGlobal.onRefresh = () => this.refreshData(true); + } + + refreshData(_force: boolean) { + rpcnSecretManagerApi.refreshSecrets(_force); + } + + cancel() { + this.secret = ''; + appGlobal.history.push('/connect-clusters/redpanda-connect-secret'); + } + + async updateSecret() { + this.isUpdating = true; + + //create function given string return base64 encoded Uint8Array + function base64Encode(str: string): Uint8Array { + const encodedString = btoa(str); + const charList = encodedString.split('').map(char => char.charCodeAt(0)); + return new Uint8Array(charList); + } + + + rpcnSecretManagerApi.update(this.props.secretId, new UpdateSecretRequest({ + id: this.props.secretId, + secretData: base64Encode(this.secret), + scopes: [Scope.REDPANDA_CONNECT] + })) + .then(async () => { + toast({ + status: 'success', duration: 4000, isClosable: false, + title: 'Secret updated' + }); + await pipelinesApi.refreshPipelines(true); + appGlobal.history.push('/connect-clusters/redpanda-connect-secret'); + }) + .catch(err => { + toast({ + status: 'error', duration: null, isClosable: true, + title: 'Failed to update secret', + description: formatPipelineError(err), + }) + }) + .finally(() => { + this.isUpdating = false; + }); + } + + render() { + if (!rpcnSecretManagerApi.secrets) return DefaultSkeleton; + + const isSecretEmpty = this.secret.trim().length == 0; + + return ( + + + + + + + + + + + + this.secret = x.target.value} + width={500} + type="password" + /> + + + + + + + + + + ); + + } +} + +export default RpConnectSecretUpdate; From dcc1289a619cc7aff6ed991174c759c2bca6dcb5 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 21 Nov 2024 18:37:27 -0500 Subject: [PATCH 04/19] console: add redpanda connect grpc client --- frontend/src/config.ts | 7 +++-- frontend/src/state/backendApi.ts | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 765e82f99..0252c8974 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -19,7 +19,7 @@ import { Interceptor as ConnectRpcInterceptor, StreamRequest, UnaryRequest, crea import { createConnectTransport } from '@connectrpc/connect-web'; import { ConsoleService } from './protogen/redpanda/api/console/v1alpha1/console_service_connect'; import { SecurityService } from './protogen/redpanda/api/console/v1alpha1/security_connect'; -// import { RedpandaConnectService } from './protogen/redpanda/api/console/v1alpha1/rp_connect_connect'; +import { SecretService as RPCNSecretService } from './protogen/redpanda/api/dataplane/v1alpha2/secret_connect'; import { PipelineService } from './protogen/redpanda/api/console/v1alpha1/pipeline_connect'; import { TransformService } from './protogen/redpanda/api/console/v1alpha1/transform_connect'; import { configureMonacoYaml } from 'monaco-yaml'; @@ -76,6 +76,7 @@ interface Config { debugBundleClient?: PromiseClient; securityClient?: PromiseClient; pipelinesClient?: PromiseClient; + rpcnSecretsClient?: PromiseClient; transformsClient?: PromiseClient; fetch: WindowOrWorkerGlobalScope['fetch']; assetsPath: string; @@ -113,6 +114,7 @@ const setConfig = ({ fetch, urlOverride, jwt, isServerless, ...args }: SetConfig const debugBundleGrpcClient = createPromiseClient(DebugBundleService, transport); const securityGrpcClient = createPromiseClient(SecurityService, transport); const pipelinesGrpcClient = createPromiseClient(PipelineService, transport); + const secretGrpcClient = createPromiseClient(RPCNSecretService, transport); const transformClient = createPromiseClient(TransformService, transport); Object.assign(config, { jwt, @@ -126,6 +128,7 @@ const setConfig = ({ fetch, urlOverride, jwt, isServerless, ...args }: SetConfig securityClient: securityGrpcClient, pipelinesClient: pipelinesGrpcClient, transformsClient: transformClient, + rpcnSecretsClient: secretGrpcClient, ...args, }); return config; @@ -140,7 +143,7 @@ export const setMonacoTheme = (_editor: monaco.editor.IStandaloneCodeEditor, mon 'editorGutter.background': '#00000018', 'editor.lineHighlightBackground': '#aaaaaa20', 'editor.lineHighlightBorder': '#00000000', - 'editorLineNumber.foreground': '#8c98a8', + 'editorLineNumber.foreground': '#8c98a8', 'editorOverviewRuler.background': '#606060', }, rules: [] diff --git a/frontend/src/state/backendApi.ts b/frontend/src/state/backendApi.ts index 77639e35e..0d64ef798 100644 --- a/frontend/src/state/backendApi.ts +++ b/frontend/src/state/backendApi.ts @@ -120,6 +120,7 @@ import { PartitionOffsetOrigin } from './ui'; import { Features } from './supportedFeatures'; import { TransformMetadata } from '../protogen/redpanda/api/dataplane/v1alpha1/transform_pb'; import { Pipeline, PipelineCreate, PipelineUpdate } from '../protogen/redpanda/api/dataplane/v1alpha2/pipeline_pb'; +import { CreateSecretRequest, DeleteSecretRequest, ListSecretsRequest, Secret, UpdateSecretRequest } from '../protogen/redpanda/api/dataplane/v1alpha2/secret_pb'; import { License, ListEnterpriseFeaturesResponse_Feature, SetLicenseRequest, SetLicenseResponse } from '../protogen/redpanda/api/console/v1alpha1/license_pb'; import { CreateDebugBundleRequest, CreateDebugBundleResponse, DebugBundleStatus, DebugBundleStatus_Status, GetClusterHealthResponse, GetDebugBundleStatusResponse_DebugBundleBrokerStatus } from '../protogen/redpanda/api/console/v1alpha1/debug_bundle_pb'; @@ -1854,6 +1855,57 @@ export const pipelinesApi = observable({ }, }); +export const rpcnSecretManagerApi = observable({ + secrets: undefined as undefined | Secret[], + + async refreshSecrets(_force: boolean): Promise { + + const client = appConfig.rpcnSecretsClient; + if (!client) throw new Error('redpanda connect secret client is not initialized'); + + const secrets = []; + + let nextPageToken = ''; + while (true) { + + const res = await client.listSecrets(new ListSecretsRequest({ + pageToken: nextPageToken, + pageSize: 100 + })); + + const response = res.secrets; + if (!response) break; + + secrets.push(...response); + + if (!res || res.nextPageToken.length == 0) + break; + nextPageToken = res.nextPageToken; + } + + this.secrets = secrets; + }, + + async delete(secret: DeleteSecretRequest) { + const client = appConfig.rpcnSecretsClient; + if (!client) throw new Error('redpanda connect secret client is not initialized'); + + await client.deleteSecret(secret); + }, + async create(secret: CreateSecretRequest) { + const client = appConfig.rpcnSecretsClient; + if (!client) throw new Error('redpanda connect secret client is not initialized'); + + await client.createSecret(secret); + }, + async update(id: string, updateSecretRequest: UpdateSecretRequest) { + const client = appConfig.rpcnSecretsClient; + if (!client) throw new Error('redpanda connect secret client is not initialized'); + + await client.updateSecret(updateSecretRequest); + }, +}); + export const transformsApi = observable({ transforms: undefined as undefined | TransformMetadata[], From 2cabe88344c7c362554d03414e3fa6e4b8bef71b Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 21 Nov 2024 18:38:25 -0500 Subject: [PATCH 05/19] console: add redpanda connect secret routes --- frontend/src/components/routes.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/routes.tsx b/frontend/src/components/routes.tsx index 3226fe020..167b80301 100644 --- a/frontend/src/components/routes.tsx +++ b/frontend/src/components/routes.tsx @@ -57,6 +57,8 @@ import {isServerless} from '../config'; import UploadLicensePage from './pages/admin/UploadLicensePage'; import RpConnectPipelinesEdit from './pages/rp-connect/Pipelines.Edit'; import AdminPageDebugBundleProgress from './pages/admin/Admin.DebugBundleProgress'; +import RpConnectSecretCreate from './pages/rp-connect/secrets/Secrets.Create'; +import RpConnectSecretUpdate from './pages/rp-connect/secrets/Secrets.Update'; // // Route Types @@ -281,7 +283,8 @@ export const APP_ROUTES: IRouteEntry[] = [ routeVisibility(true, [Feature.GetQuotas], ['canListQuotas']) ), - MakeRoute<{}>('/connect-clusters', KafkaConnectOverview, 'Connect', LinkIcon, true, + // defaultView difines the default tab to show when the user navigates to the page + MakeRoute<{defaultView: string}>('/connect-clusters/:defaultView?', KafkaConnectOverview, 'Connect', LinkIcon, true, () => { if (isServerless()) { console.log('Connect clusters inside serverless checks.') @@ -315,9 +318,11 @@ export const APP_ROUTES: IRouteEntry[] = [ MakeRoute<{ transformName: string }>('/transforms/:transformName', TransformDetails, 'Transforms'), // MakeRoute<{}>('/rp-connect', RpConnectPipelinesList, 'Connectors', LinkIcon, true), + MakeRoute<{}>('/rp-connect/secret/create', RpConnectSecretCreate, 'Connector-Secrets'), MakeRoute<{}>('/rp-connect/create', RpConnectPipelinesCreate, 'Connectors'), MakeRoute<{ pipelineId: string }>('/rp-connect/:pipelineId', RpConnectPipelinesDetails, 'Connectors'), MakeRoute<{ pipelineId: string }>('/rp-connect/:pipelineId/edit', RpConnectPipelinesEdit, 'Connectors'), + MakeRoute<{ secretId: string }>('/rp-connect/secret/:secretId/edit', RpConnectSecretUpdate, 'Connector-Secrets'), MakeRoute<{}>('/reassign-partitions', ReassignPartitions, 'Reassign Partitions', BeakerIcon, false, routeVisibility(true, From d87484c19628cc57e926b3cffdf4ca917bf77704 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 21 Nov 2024 18:40:24 -0500 Subject: [PATCH 06/19] console: use same design buttons as last secret manager design --- .../pages/rp-connect/Pipelines.List.tsx | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/Pipelines.List.tsx b/frontend/src/components/pages/rp-connect/Pipelines.List.tsx index f8fc54fcb..277a77641 100644 --- a/frontend/src/components/pages/rp-connect/Pipelines.List.tsx +++ b/frontend/src/components/pages/rp-connect/Pipelines.List.tsx @@ -35,6 +35,19 @@ import { HiX } from 'react-icons/hi'; const { ToastContainer, toast } = createStandaloneToast(); +const CreatePipelineButton = () => { + return ( + + ) +} + +const EmptyPlaceholder = () => { + return + + You have no Redpanda Connect pipelines. + + +}; export const PipelineStatus = observer((p: { status: Pipeline_State }) => { switch (p.status) { @@ -121,17 +134,15 @@ class RpConnectPipelinesList extends PageComponent<{}> { {/* Pipeline List */} -
- -
- - + uiSettings.pipelinesList.quickSearch = x} - placeholderText="Enter search term / regex..." + searchText={uiSettings.pipelinesList.quickSearch} + setSearchText={x => uiSettings.pipelinesList.quickSearch = x} + placeholderText="Enter search term / regex..." /> - + + + {(pipelinesApi.pipelines ?? []).length == 0 ? @@ -226,13 +237,3 @@ class RpConnectPipelinesList extends PageComponent<{}> { } export default RpConnectPipelinesList; - -const EmptyPlaceholder = () => { - return - - You have no Redpanda Connect pipelines. - - - - -}; From 6bbb7c317249c703b48d1b85731dffad16dfb1bb Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 21 Nov 2024 18:41:42 -0500 Subject: [PATCH 07/19] console: add secret tab in redpanda connect page --- .../src/components/pages/connect/Overview.tsx | 59 ++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/pages/connect/Overview.tsx b/frontend/src/components/pages/connect/Overview.tsx index f36e26d4d..488c31a9a 100644 --- a/frontend/src/components/pages/connect/Overview.tsx +++ b/frontend/src/components/pages/connect/Overview.tsx @@ -27,9 +27,36 @@ import RpConnectPipelinesList from '../rp-connect/Pipelines.List'; import { RedpandaConnectIntro } from '../rp-connect/RedpandaConnectIntro'; import { Features } from '../../../state/supportedFeatures'; import {isServerless} from '../../../config'; +import RpConnectSecretsList from '../rp-connect/secrets/Secrets.List'; + +enum ConnectView { + kafkaConnect = 'kafka-connect', + RedpandaConnect = 'redpanda-connect', + RedpandaConncetSecret = 'redpanda-connect-secret', +} + +const getDefaultView = (defaultView: string): ConnectView => { + + const showPipelines = Features.pipelinesApi + + if (!showPipelines) { + return ConnectView.kafkaConnect; + } + + switch (defaultView) { + case 'kafka-connect': + return ConnectView.kafkaConnect; + case 'redpanda-connect': + return ConnectView.RedpandaConnect; + case 'redpanda-connect-secret': + return ConnectView.RedpandaConncetSecret; + default: + return showPipelines ? ConnectView.RedpandaConnect : ConnectView.kafkaConnect; + } +} @observer -class KafkaConnectOverview extends PageComponent { +class KafkaConnectOverview extends PageComponent<{ defaultView: string }> { initPage(p: PageInitHelper): void { p.title = 'Overview'; p.addBreadcrumb('Connect', '/connect-clusters'); @@ -54,18 +81,18 @@ class KafkaConnectOverview extends PageComponent { const tabs = [ { - key: 'redpandaConnect', + key: ConnectView.RedpandaConnect, title: Redpanda Connect Recommended, content: Redpanda Connect is an alternative to Kafka Connect. Choose from a growing ecosystem of readily available connectors. Learn more. - + , }, { - key: 'kafkaConnect', + key: ConnectView.kafkaConnect, title: Kafka Connect, content: @@ -89,7 +116,7 @@ class KafkaConnectOverview extends PageComponent { ? tabs[0].content() : tabs[0].content ) - : + : } ); @@ -326,11 +353,25 @@ const TabKafkaConnect = observer((_p: {}) => { }) -const TabRedpandaConnect = observer((_p: {}) => { +const TabRedpandaConnect = observer((_p: {defaultView: ConnectView}) => { if (!Features.pipelinesApi) // If the backend doesn't support pipelines, show the intro page - return - - return + return + + const tabs = [ + { + key: 'pipelines', + title: Pipelines, + content: , + }, + { + key: 'secrets', + title: Secrets, + content: + + }, + ] as Tab[]; + + return }) export type ConnectTabKeys = 'clusters' | 'connectors' | 'tasks'; From 390fddf5c8e032fd1e48c7487b0e59a851b7f5da Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 21 Nov 2024 18:48:44 -0500 Subject: [PATCH 08/19] console: add autocomplete secrets to create pipeline --- .../pages/rp-connect/Pipelines.Create.tsx | 70 +++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/Pipelines.Create.tsx b/frontend/src/components/pages/rp-connect/Pipelines.Create.tsx index a1885fe04..2dd19bf06 100644 --- a/frontend/src/components/pages/rp-connect/Pipelines.Create.tsx +++ b/frontend/src/components/pages/rp-connect/Pipelines.Create.tsx @@ -17,13 +17,16 @@ import PageContent from '../../misc/PageContent'; import { PageComponent, PageInitHelper } from '../Page'; import { Alert, AlertIcon, Box, Button, Text, createStandaloneToast, Flex, FormField, Input } from '@redpanda-data/ui'; import PipelinesYamlEditor from '../../misc/PipelinesYamlEditor'; -import { pipelinesApi } from '../../../state/backendApi'; +import {pipelinesApi, rpcnSecretManagerApi} from '../../../state/backendApi'; import { DefaultSkeleton } from '../../../utils/tsxUtils'; import { Link } from 'react-router-dom'; import { Link as ChLink } from '@redpanda-data/ui'; import Tabs from '../../misc/tabs/Tabs'; import { PipelineCreate } from '../../../protogen/redpanda/api/dataplane/v1alpha2/pipeline_pb'; import { formatPipelineError } from './errors'; +import {Monaco} from '@monaco-editor/react'; +import {editor, languages, Position} from 'monaco-editor'; +import CompletionItem = languages.CompletionItem; const { ToastContainer, toast } = createStandaloneToast(); const exampleContent = ` @@ -36,7 +39,7 @@ class RpConnectPipelinesCreate extends PageComponent<{}> { @observable description = ''; @observable editorContent = exampleContent; @observable isCreating = false; - + @observable secrets: string[] = [] constructor(p: any) { super(p); makeObservable(this, undefined, { autoBind: true }); @@ -48,6 +51,8 @@ class RpConnectPipelinesCreate extends PageComponent<{}> { p.addBreadcrumb('Create Pipeline', ''); this.refreshData(true); + // get secrets + rpcnSecretManagerApi.refreshSecrets(true); appGlobal.onRefresh = () => this.refreshData(true); } @@ -58,7 +63,10 @@ class RpConnectPipelinesCreate extends PageComponent<{}> { render() { if (!pipelinesApi.pipelines) return DefaultSkeleton; - + if (rpcnSecretManagerApi.secrets) { + // inject secrets to editor + this.secrets.updateWith(rpcnSecretManagerApi.secrets.map(value => value.id)) + } const alreadyExists = pipelinesApi.pipelines.any(x => x.id == this.fileName); const isNameEmpty = this.fileName.trim().length == 0; @@ -99,7 +107,7 @@ class RpConnectPipelinesCreate extends PageComponent<{}> { - this.editorContent = x} /> + this.editorContent = x} secrets={this.secrets}/> @@ -156,7 +164,60 @@ export default RpConnectPipelinesCreate; export const PipelineEditor = observer((p: { yaml: string, onChange: (newYaml: string) => void + secrets?: string[] }) => { + const secrets = p.secrets ?? []; + // add custom autocomplete for ${secrets.} + const addCustomAutocomplete = (monaco: Monaco) => { + monaco.languages.registerCompletionItemProvider('yaml', { + // Display suggestions for lines beginning with ${ or . to assist with variable interpolation + triggerCharacters: ['${', '.'], + provideCompletionItems(model: editor.ITextModel, position: Position): languages.ProviderResult { + const wordInfo = model.getWordUntilPosition(position); + const previousInfo = model.getWordUntilPosition({lineNumber: position.lineNumber, column: wordInfo.startColumn - 1}); + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: wordInfo.startColumn, + endColumn: wordInfo.endColumn, + }; + const last_chars = model.getValueInRange({startLineNumber: position.lineNumber, startColumn: 0, endLineNumber: position.lineNumber, endColumn: position.column}); + const words = last_chars.replace('\t', '').replace('\{', '').split(' '); + const active_typing = words[words.length - 1]; + const empty = {suggestions: []} + // don't show suggestion if previous word is a secret + if (secrets.some(value => value === previousInfo.word)) { + return empty + } + // don't show suggestion if there is multiples dots(.) + if (/\.{2,}$/.test(active_typing)) { + return empty + } + // if previous word is secrets suggest secrets ids + if (previousInfo.word === 'secrets') { + const suggestions: CompletionItem[] = secrets.map(value => ({ + label: value, // First option + kind: monaco.languages.CompletionItemKind.Class, + insertText: value, // Insert this text + range: range, + })) + return {suggestions} + } + // no previous word, suggest secrets + const suggestions: CompletionItem[] = [ + { + label: 'secrets', // First option + kind: monaco.languages.CompletionItemKind.Variable, + insertText: 'secrets', // Insert this text + documentation: 'redpanda connect secrets', + range: range, + }, + ] + + return {suggestions} + } + }) + } return addCustomAutocomplete(monaco)} /> {isKafkaConnectPipeline(p.yaml) && From b8b41cc5b8ab29f713e8f70c81e72010909af130 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 21 Nov 2024 19:01:18 -0500 Subject: [PATCH 09/19] console: add autocomplete secrets to edit pipeline --- .../components/pages/rp-connect/Pipelines.Edit.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/Pipelines.Edit.tsx b/frontend/src/components/pages/rp-connect/Pipelines.Edit.tsx index 5516e835e..7d40714cd 100644 --- a/frontend/src/components/pages/rp-connect/Pipelines.Edit.tsx +++ b/frontend/src/components/pages/rp-connect/Pipelines.Edit.tsx @@ -16,7 +16,7 @@ import { appGlobal } from '../../../state/appGlobal'; import PageContent from '../../misc/PageContent'; import { PageComponent, PageInitHelper } from '../Page'; import { Box, Button, createStandaloneToast, Flex, FormField, Input } from '@redpanda-data/ui'; -import { pipelinesApi } from '../../../state/backendApi'; +import {pipelinesApi, rpcnSecretManagerApi} from '../../../state/backendApi'; import { DefaultSkeleton } from '../../../utils/tsxUtils'; import { Link } from 'react-router-dom'; import { PipelineUpdate } from '../../../protogen/redpanda/api/dataplane/v1alpha2/pipeline_pb'; @@ -33,6 +33,7 @@ class RpConnectPipelinesEdit extends PageComponent<{ pipelineId: string }> { @observable description = undefined as unknown as string; @observable editorContent = undefined as unknown as string; @observable isUpdating = false; + @observable secrets: string[] = [] constructor(p: any) { super(p); @@ -47,6 +48,8 @@ class RpConnectPipelinesEdit extends PageComponent<{ pipelineId: string }> { p.addBreadcrumb('Edit Pipeline', `/rp-connect/${pipelineId}/edit`); this.refreshData(true); + // get secrets + rpcnSecretManagerApi.refreshSecrets(true); appGlobal.onRefresh = () => this.refreshData(true); } @@ -57,7 +60,10 @@ class RpConnectPipelinesEdit extends PageComponent<{ pipelineId: string }> { render() { if (!pipelinesApi.pipelines) return DefaultSkeleton; - + if (rpcnSecretManagerApi.secrets) { + // inject secrets to editor + this.secrets.updateWith(rpcnSecretManagerApi.secrets.map(value => value.id)) + } const pipelineId = this.props.pipelineId; const pipeline = pipelinesApi.pipelines.first(x => x.id == pipelineId); if (!pipeline) return DefaultSkeleton; @@ -104,7 +110,7 @@ class RpConnectPipelinesEdit extends PageComponent<{ pipelineId: string }> { - this.editorContent = x} /> + this.editorContent = x} secrets={this.secrets} /> From 8e906013bf98e25fa0ac4f15a1fd3d376fdd7ec3 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 21 Nov 2024 19:23:37 -0500 Subject: [PATCH 10/19] console: add query parameter to select the default Connect tab to open. --- .../src/components/pages/connect/Overview.tsx | 50 ++++++++++++------- .../rp-connect/secrets/Secrets.Create.tsx | 11 ++-- .../pages/rp-connect/secrets/Secrets.List.tsx | 13 +---- .../rp-connect/secrets/Secrets.Update.tsx | 6 ++- frontend/src/components/routes.tsx | 9 ++-- 5 files changed, 48 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/pages/connect/Overview.tsx b/frontend/src/components/pages/connect/Overview.tsx index 488c31a9a..f67cc0eb6 100644 --- a/frontend/src/components/pages/connect/Overview.tsx +++ b/frontend/src/components/pages/connect/Overview.tsx @@ -10,14 +10,14 @@ */ import { observer, useLocalObservable } from 'mobx-react'; -import { Component } from 'react'; +import {Component, FunctionComponent} from 'react'; import { appGlobal } from '../../../state/appGlobal'; import { api } from '../../../state/backendApi'; import { ClusterConnectorInfo, ClusterConnectors, ClusterConnectorTaskInfo } from '../../../state/restInterfaces'; import { uiSettings } from '../../../state/ui'; import { Code, DefaultSkeleton } from '../../../utils/tsxUtils'; import Tabs, { Tab } from '../../misc/tabs/Tabs'; -import { PageComponent, PageInitHelper } from '../Page'; +import {PageComponent, PageInitHelper} from '../Page'; import { ConnectorClass, ConnectorsColumn, errIcon, mr05, NotConfigured, OverviewStatisticsCard, TasksColumn, TaskState } from './helper'; import Section from '../../misc/Section'; import PageContent from '../../misc/PageContent'; @@ -28,33 +28,47 @@ import { RedpandaConnectIntro } from '../rp-connect/RedpandaConnectIntro'; import { Features } from '../../../state/supportedFeatures'; import {isServerless} from '../../../config'; import RpConnectSecretsList from '../rp-connect/secrets/Secrets.List'; +import { useLocation } from 'react-router-dom'; enum ConnectView { - kafkaConnect = 'kafka-connect', + KafkaConnect = 'kafka-connect', RedpandaConnect = 'redpanda-connect', - RedpandaConncetSecret = 'redpanda-connect-secret', + RedpandaConnectSecret = 'redpanda-connect-secret', } -const getDefaultView = (defaultView: string): ConnectView => { +/** + * The Redpanda Connect Secret Manager introduces a new tab in Redpanda Connect. + * this logic determines which tab should be opened based on the `defaultTab` + * query parameter in the URL. + */ +const getDefaultView = (defaultView: string): { initialTab: ConnectView, redpandaConnectTab: ConnectView } => { const showPipelines = Features.pipelinesApi - + const showKafkaTab = {initialTab: ConnectView.KafkaConnect, redpandaConnectTab: ConnectView.RedpandaConnect} + const showRedpandaConnectTab = {initialTab: ConnectView.RedpandaConnect, redpandaConnectTab: ConnectView.RedpandaConnect} if (!showPipelines) { - return ConnectView.kafkaConnect; + return showKafkaTab; } switch (defaultView) { case 'kafka-connect': - return ConnectView.kafkaConnect; + return showKafkaTab; case 'redpanda-connect': - return ConnectView.RedpandaConnect; + return showRedpandaConnectTab; case 'redpanda-connect-secret': - return ConnectView.RedpandaConncetSecret; + return {initialTab: ConnectView.RedpandaConnect, redpandaConnectTab: ConnectView.RedpandaConnectSecret}; default: - return showPipelines ? ConnectView.RedpandaConnect : ConnectView.kafkaConnect; + return showRedpandaConnectTab; } } +const WrapUseSearchParamsHook: FunctionComponent<{matchedPath: string}> = (props) => { + const {search}= useLocation(); + const searchParams = new URLSearchParams(search); + const defaultTab = searchParams.get('defaultTab') || ''; + return +} + @observer class KafkaConnectOverview extends PageComponent<{ defaultView: string }> { initPage(p: PageInitHelper): void { @@ -77,8 +91,6 @@ class KafkaConnectOverview extends PageComponent<{ defaultView: string }> { } render() { - const showPipelines = Features.pipelinesApi - const tabs = [ { key: ConnectView.RedpandaConnect, @@ -88,11 +100,11 @@ class KafkaConnectOverview extends PageComponent<{ defaultView: string }> { Redpanda Connect is an alternative to Kafka Connect. Choose from a growing ecosystem of readily available connectors. Learn more. - + , }, { - key: ConnectView.kafkaConnect, + key: ConnectView.KafkaConnect, title: Kafka Connect, content: @@ -104,7 +116,7 @@ class KafkaConnectOverview extends PageComponent<{ defaultView: string }> { ] as Tab[]; if (isServerless()) - tabs.removeAll(x => x.key == 'kafkaConnect'); + tabs.removeAll(x => x.key == ConnectView.KafkaConnect); return ( @@ -116,14 +128,14 @@ class KafkaConnectOverview extends PageComponent<{ defaultView: string }> { ? tabs[0].content() : tabs[0].content ) - : + : } ); } } -export default KafkaConnectOverview; +export default WrapUseSearchParamsHook; @observer class TabClusters extends Component { @@ -371,7 +383,7 @@ const TabRedpandaConnect = observer((_p: {defaultView: ConnectView}) => { }, ] as Tab[]; - return + return }) export type ConnectTabKeys = 'clusters' | 'connectors' | 'tasks'; diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx index 9c6b24c43..50294b4c1 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx @@ -11,6 +11,9 @@ import {CreateSecretRequest, Scope} from '../../../../protogen/redpanda/api/data const {ToastContainer, toast} = createStandaloneToast(); +const returnSecretTab = '/connect-clusters?defaultTab=redpanda-connect-secret' + + @observer class RpConnectSecretCreate extends PageComponent { @@ -39,7 +42,7 @@ class RpConnectSecretCreate extends PageComponent { cancel() { this.secret = ''; this.id = ''; - appGlobal.history.push('/connect-clusters/redpanda-connect-secret'); + appGlobal.history.push(returnSecretTab); } async createSecret() { @@ -61,15 +64,15 @@ class RpConnectSecretCreate extends PageComponent { .then(async () => { toast({ status: 'success', duration: 4000, isClosable: false, - title: 'Pipeline created' + title: 'Secret created' }); await pipelinesApi.refreshPipelines(true); - appGlobal.history.push('/connect-clusters/redpanda-connect-secret'); + appGlobal.history.push(returnSecretTab); }) .catch(err => { toast({ status: 'error', duration: null, isClosable: true, - title: 'Failed to create pipeline', + title: 'Failed to create secret', description: formatPipelineError(err), }) }) diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx index 37269d63e..d7f0a20f7 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx @@ -9,7 +9,6 @@ import Section from '../../../misc/Section'; import PageContent from '../../../misc/PageContent'; import {uiSettings} from '../../../../state/ui'; import {Link} from 'react-router-dom'; -import {encodeURIComponentPercents} from '../../../../utils/utils'; import {PencilIcon, TrashIcon} from '@heroicons/react/outline'; import EmptyConnectors from '../../../../assets/redpanda/EmptyConnectors.svg'; import {DeleteSecretRequest, Secret} from '../../../../protogen/redpanda/api/dataplane/v1alpha2/secret_pb'; @@ -108,20 +107,12 @@ class RpConnectSecretsList extends PageComponent { columns={[ { header: 'Secret name', - cell: ({row: {original}}) => ( - - {original.id} - - ), + cell: ({row: {original}}) => {original.id}, size: 200, }, { header: 'Secret notation', - cell: ({row: {original}}) => ( - - {`$(secrets.${original.id})`} - - ), + cell: ({row: {original}}) => {`$(secrets.${original.id})`}, size: 400 }, // let use this on next phase diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx index 23948007c..11d37b4d7 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx @@ -11,6 +11,8 @@ import {Scope, UpdateSecretRequest} from '../../../../protogen/redpanda/api/data const {ToastContainer, toast} = createStandaloneToast(); +const returnSecretTab = '/connect-clusters?defaultTab=redpanda-connect-secret' + @observer class RpConnectSecretUpdate extends PageComponent<{ secretId: string }> { @observable secret = ''; @@ -36,7 +38,7 @@ class RpConnectSecretUpdate extends PageComponent<{ secretId: string }> { cancel() { this.secret = ''; - appGlobal.history.push('/connect-clusters/redpanda-connect-secret'); + appGlobal.history.push(returnSecretTab); } async updateSecret() { @@ -61,7 +63,7 @@ class RpConnectSecretUpdate extends PageComponent<{ secretId: string }> { title: 'Secret updated' }); await pipelinesApi.refreshPipelines(true); - appGlobal.history.push('/connect-clusters/redpanda-connect-secret'); + appGlobal.history.push(returnSecretTab); }) .catch(err => { toast({ diff --git a/frontend/src/components/routes.tsx b/frontend/src/components/routes.tsx index 167b80301..e7c8bc23e 100644 --- a/frontend/src/components/routes.tsx +++ b/frontend/src/components/routes.tsx @@ -9,7 +9,7 @@ * by the Apache License, Version 2.0 */ -import React from 'react'; +import React, {FunctionComponent} from 'react'; import { Switch } from 'react-router-dom'; import { Section } from './misc/common'; import { Route, Redirect } from 'react-router'; @@ -68,7 +68,7 @@ type IRouteEntry = PageDefinition; export interface PageDefinition { title: string; path: string; - pageType: PageComponentType; + pageType: PageComponentType | FunctionComponent; routeJsx: JSX.Element; icon?: (props: React.ComponentProps<'svg'>) => JSX.Element; menuItemKey?: string; // set by 'CreateRouteMenuItems' @@ -157,7 +157,7 @@ interface MenuItemState { function MakeRoute( path: string, - page: PageComponentType, + page: PageComponentType | FunctionComponent, title: string, icon?: (props: React.ComponentProps<'svg'>) => JSX.Element, exact: boolean = true, showCallback?: () => MenuItemState): PageDefinition { @@ -283,8 +283,7 @@ export const APP_ROUTES: IRouteEntry[] = [ routeVisibility(true, [Feature.GetQuotas], ['canListQuotas']) ), - // defaultView difines the default tab to show when the user navigates to the page - MakeRoute<{defaultView: string}>('/connect-clusters/:defaultView?', KafkaConnectOverview, 'Connect', LinkIcon, true, + MakeRoute<{matchedPath: string}>('/connect-clusters', KafkaConnectOverview, 'Connect', LinkIcon, true, () => { if (isServerless()) { console.log('Connect clusters inside serverless checks.') From 837d6e546fb225ecd335d100ed2e4738a85f7d24 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Sun, 24 Nov 2024 15:13:50 -0500 Subject: [PATCH 11/19] rp-connect: hide search field if there are no secret or pipeline --- .../pages/rp-connect/Pipelines.List.tsx | 21 +++-- .../pages/rp-connect/secrets/Secrets.List.tsx | 82 ++++++++++--------- 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/Pipelines.List.tsx b/frontend/src/components/pages/rp-connect/Pipelines.List.tsx index 277a77641..6d3930fb6 100644 --- a/frontend/src/components/pages/rp-connect/Pipelines.List.tsx +++ b/frontend/src/components/pages/rp-connect/Pipelines.List.tsx @@ -37,7 +37,7 @@ const { ToastContainer, toast } = createStandaloneToast(); const CreatePipelineButton = () => { return ( - + ) } @@ -134,14 +134,17 @@ class RpConnectPipelinesList extends PageComponent<{}> { {/* Pipeline List */} - - uiSettings.pipelinesList.quickSearch = x} - placeholderText="Enter search term / regex..." - /> - - + {pipelinesApi.pipelines.length != 0 && ( + + + uiSettings.pipelinesList.quickSearch = x} + placeholderText="Enter search term / regex..." + /> + + )} + {(pipelinesApi.pipelines ?? []).length == 0 diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx index d7f0a20f7..019808d33 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx @@ -17,7 +17,7 @@ const {ToastContainer, toast} = createStandaloneToast(); const CreateSecretButton = () => { return ( - + ) } @@ -88,14 +88,16 @@ class RpConnectSecretsList extends PageComponent {
- - uiSettings.rpncSecretList.quickSearch = x} - placeholderText="Enter search term / regex..." - /> - - + {rpcnSecretManagerApi.secrets?.length != 0 && ( + + + uiSettings.rpncSecretList.quickSearch = x} + placeholderText="Enter search term / regex..." + /> + + )} {(rpcnSecretManagerApi.secrets ?? []).length == 0 ? @@ -112,7 +114,7 @@ class RpConnectSecretsList extends PageComponent { }, { header: 'Secret notation', - cell: ({row: {original}}) => {`$(secrets.${original.id})`}, + cell: ({row: {original}}) => {`$(secrets.${original.id})`}, size: 400 }, // let use this on next phase @@ -127,34 +129,40 @@ class RpConnectSecretsList extends PageComponent { header: '', id: 'actions', cell: ({row: {original: r}}) => - - - - - } itemType={'Secret'} - onConfirm={ - async (dismiss) => { - await this.deleteSecret(r.id) - dismiss(); + + + + + + } itemType={'Secret'} + onConfirm={ + async (dismiss) => { + await this.deleteSecret(r.id) + dismiss(); + } } - }> - Deleting this secret may disrupt the functionality of pipelines that depend on it. Are you sure you want to delete the secret {r.id}? - - - + inputMatchText={r.id}> + + Deleting this secret may disrupt the functionality of pipelines that depend on it. Are you sure? + To confirm, type {r.id} in the confirmation box below. + + + + + , size: 10 }, From deaac9aab92792dd43aebd9d9b72052b19c2dbbd Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Sun, 24 Nov 2024 15:20:54 -0500 Subject: [PATCH 12/19] rp-connect: add password input for secret values. --- .../pages/rp-connect/secrets/Secrets.Create.tsx | 8 ++++---- .../pages/rp-connect/secrets/Secrets.Update.tsx | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx index 50294b4c1..820c2ce71 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx @@ -1,4 +1,4 @@ -import {Button, ButtonGroup, createStandaloneToast, Flex, FormField, Input} from '@redpanda-data/ui'; +import {Button, ButtonGroup, createStandaloneToast, Flex, FormField, Input, PasswordInput} from '@redpanda-data/ui'; import {PageComponent, PageInitHelper} from '../../Page'; import {observer} from 'mobx-react'; import {appGlobal} from '../../../../state/appGlobal'; @@ -110,8 +110,8 @@ class RpConnectSecretCreate extends PageComponent { - - + this.secret = x.target.value} width={500} type="password" - disabled={this.isCreating} + isDisabled={this.isCreating} /> diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx index 11d37b4d7..5b7702f8e 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx @@ -1,4 +1,4 @@ -import {Button, ButtonGroup, createStandaloneToast, Flex, FormField, Input} from '@redpanda-data/ui'; +import {Button, ButtonGroup, createStandaloneToast, Flex, FormField, Input, PasswordInput} from '@redpanda-data/ui'; import {PageComponent, PageInitHelper} from '../../Page'; import {observer} from 'mobx-react'; import {appGlobal} from '../../../../state/appGlobal'; @@ -91,7 +91,7 @@ class RpConnectSecretUpdate extends PageComponent<{ secretId: string }> { { - - + { onChange={x => this.secret = x.target.value} width={500} type="password" + isDisabled={this.isUpdating} /> From 94e4bc6da5f275d9ef60d699590344c972de992e Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Mon, 25 Nov 2024 15:24:29 -0500 Subject: [PATCH 13/19] connect: add copy button to secret list --- .../pages/rp-connect/secrets/Secrets.List.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx index 019808d33..262b8588a 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx @@ -4,7 +4,7 @@ import {observer} from 'mobx-react'; import {appGlobal} from '../../../../state/appGlobal'; import {rpcnSecretManagerApi} from '../../../../state/backendApi'; import {Features} from '../../../../state/supportedFeatures'; -import {Box, Button, ButtonGroup, Code, ConfirmItemDeleteModal, createStandaloneToast, DataTable, Flex, Image, SearchField, Text} from '@redpanda-data/ui'; +import {Box, Button, ButtonGroup, Code, ConfirmItemDeleteModal, CopyButton, createStandaloneToast, DataTable, Flex, Image, SearchField, Text, Tooltip} from '@redpanda-data/ui'; import Section from '../../../misc/Section'; import PageContent from '../../../misc/PageContent'; import {uiSettings} from '../../../../state/ui'; @@ -89,7 +89,7 @@ class RpConnectSecretsList extends PageComponent { {rpcnSecretManagerApi.secrets?.length != 0 && ( - + {`$(secrets.${original.id})`}, + cell: ({row: {original}}) => ( + + {`$\{secrets.${original.id}}`} + + + + ), size: 400 }, // let use this on next phase From d03be2e1341cf5e6f3a4483bdd452ceb3adcc398 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Tue, 26 Nov 2024 10:23:46 -0500 Subject: [PATCH 14/19] pipeline-editor: disable secret autocomplete --- .../pages/rp-connect/Pipelines.Create.tsx | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/Pipelines.Create.tsx b/frontend/src/components/pages/rp-connect/Pipelines.Create.tsx index 2dd19bf06..29bf075f1 100644 --- a/frontend/src/components/pages/rp-connect/Pipelines.Create.tsx +++ b/frontend/src/components/pages/rp-connect/Pipelines.Create.tsx @@ -24,9 +24,6 @@ import { Link as ChLink } from '@redpanda-data/ui'; import Tabs from '../../misc/tabs/Tabs'; import { PipelineCreate } from '../../../protogen/redpanda/api/dataplane/v1alpha2/pipeline_pb'; import { formatPipelineError } from './errors'; -import {Monaco} from '@monaco-editor/react'; -import {editor, languages, Position} from 'monaco-editor'; -import CompletionItem = languages.CompletionItem; const { ToastContainer, toast } = createStandaloneToast(); const exampleContent = ` @@ -166,59 +163,6 @@ export const PipelineEditor = observer((p: { onChange: (newYaml: string) => void secrets?: string[] }) => { - const secrets = p.secrets ?? []; - // add custom autocomplete for ${secrets.} - const addCustomAutocomplete = (monaco: Monaco) => { - monaco.languages.registerCompletionItemProvider('yaml', { - // Display suggestions for lines beginning with ${ or . to assist with variable interpolation - triggerCharacters: ['${', '.'], - provideCompletionItems(model: editor.ITextModel, position: Position): languages.ProviderResult { - const wordInfo = model.getWordUntilPosition(position); - const previousInfo = model.getWordUntilPosition({lineNumber: position.lineNumber, column: wordInfo.startColumn - 1}); - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: wordInfo.startColumn, - endColumn: wordInfo.endColumn, - }; - const last_chars = model.getValueInRange({startLineNumber: position.lineNumber, startColumn: 0, endLineNumber: position.lineNumber, endColumn: position.column}); - const words = last_chars.replace('\t', '').replace('\{', '').split(' '); - const active_typing = words[words.length - 1]; - const empty = {suggestions: []} - // don't show suggestion if previous word is a secret - if (secrets.some(value => value === previousInfo.word)) { - return empty - } - // don't show suggestion if there is multiples dots(.) - if (/\.{2,}$/.test(active_typing)) { - return empty - } - // if previous word is secrets suggest secrets ids - if (previousInfo.word === 'secrets') { - const suggestions: CompletionItem[] = secrets.map(value => ({ - label: value, // First option - kind: monaco.languages.CompletionItemKind.Class, - insertText: value, // Insert this text - range: range, - })) - return {suggestions} - } - // no previous word, suggest secrets - const suggestions: CompletionItem[] = [ - { - label: 'secrets', // First option - kind: monaco.languages.CompletionItemKind.Variable, - insertText: 'secrets', // Insert this text - documentation: 'redpanda connect secrets', - range: range, - }, - ] - - return {suggestions} - } - }) - } - return addCustomAutocomplete(monaco)} /> {isKafkaConnectPipeline(p.yaml) && From b329bba86b4481287952c480e178148215953724 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Tue, 26 Nov 2024 10:45:59 -0500 Subject: [PATCH 15/19] rpcn-secret: fix typo --- .../components/pages/rp-connect/secrets/Secrets.List.tsx | 6 +++--- frontend/src/state/ui.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx index 262b8588a..9fd444759 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx @@ -71,7 +71,7 @@ class RpConnectSecretsList extends PageComponent { const filteredSecrets = (rpcnSecretManagerApi.secrets ?? []) .filter(u => { - const filter = uiSettings.rpncSecretList.quickSearch; + const filter = uiSettings.rpcnSecretList.quickSearch; if (!filter) return true; try { const quickSearchRegExp = new RegExp(filter, 'i'); @@ -92,8 +92,8 @@ class RpConnectSecretsList extends PageComponent { uiSettings.rpncSecretList.quickSearch = x} + searchText={uiSettings.rpcnSecretList.quickSearch} + setSearchText={x => uiSettings.rpcnSecretList.quickSearch = x} placeholderText="Enter search term / regex..." /> diff --git a/frontend/src/state/ui.ts b/frontend/src/state/ui.ts index dffff6f67..9e11db530 100644 --- a/frontend/src/state/ui.ts +++ b/frontend/src/state/ui.ts @@ -234,7 +234,7 @@ const defaultUiSettings = { quickSearch: '' }, - rpncSecretList: { + rpcnSecretList: { quickSearch: '' }, From b7f33cadab545f7db1a557f026d059ab2422d830 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Wed, 27 Nov 2024 13:26:10 -0500 Subject: [PATCH 16/19] rpcn-secret: use util base64 encode fn --- .../pages/rp-connect/secrets/Secrets.Create.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx index 820c2ce71..39a1a84b7 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx @@ -8,6 +8,7 @@ import {action, makeObservable, observable} from 'mobx'; import {DefaultSkeleton} from '../../../../utils/tsxUtils'; import {formatPipelineError} from '../errors'; import {CreateSecretRequest, Scope} from '../../../../protogen/redpanda/api/dataplane/v1alpha2/secret_pb'; +import {base64ToUInt8Array, encodeBase64} from '../../../../utils/utils'; const {ToastContainer, toast} = createStandaloneToast(); @@ -48,17 +49,9 @@ class RpConnectSecretCreate extends PageComponent { async createSecret() { this.isCreating = true; - //create function given string return base64 encoded Uint8Array - function base64Encode(str: string): Uint8Array { - const encodedString = btoa(str); - const charList = encodedString.split('').map(char => char.charCodeAt(0)); - return new Uint8Array(charList); - } - - rpcnSecretManagerApi.create(new CreateSecretRequest({ id: this.id, - secretData: base64Encode(this.secret), + secretData: base64ToUInt8Array(encodeBase64(this.secret)), scopes: [Scope.REDPANDA_CONNECT] })) .then(async () => { From 5421d5ad3a0c3763c47ff7321fa3f156bb2a6e04 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Wed, 27 Nov 2024 13:26:51 -0500 Subject: [PATCH 17/19] rpcn-secret: use correct style for button --- .../components/pages/rp-connect/secrets/Secrets.List.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx index 9fd444759..37753c88d 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx @@ -8,7 +8,7 @@ import {Box, Button, ButtonGroup, Code, ConfirmItemDeleteModal, CopyButton, crea import Section from '../../../misc/Section'; import PageContent from '../../../misc/PageContent'; import {uiSettings} from '../../../../state/ui'; -import {Link} from 'react-router-dom'; +import {Link as ReactRouterLink} from 'react-router-dom'; import {PencilIcon, TrashIcon} from '@heroicons/react/outline'; import EmptyConnectors from '../../../../assets/redpanda/EmptyConnectors.svg'; import {DeleteSecretRequest, Secret} from '../../../../protogen/redpanda/api/dataplane/v1alpha2/secret_pb'; @@ -16,9 +16,9 @@ import {DeleteSecretRequest, Secret} from '../../../../protogen/redpanda/api/dat const {ToastContainer, toast} = createStandaloneToast(); const CreateSecretButton = () => { - return ( - - ) + return ( + + ) } const EmptyPlaceholder = () => { From 05837eebeec9f35cb4489f14c8bd6b770b231ce1 Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Wed, 27 Nov 2024 13:27:39 -0500 Subject: [PATCH 18/19] rpcn-secret: change secrets on url instead of secret --- .../components/pages/rp-connect/Pipelines.List.tsx | 2 +- .../pages/rp-connect/secrets/Secrets.Create.tsx | 12 ++++++------ .../pages/rp-connect/secrets/Secrets.List.tsx | 2 +- .../pages/rp-connect/secrets/Secrets.Update.tsx | 2 +- frontend/src/components/routes.tsx | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/Pipelines.List.tsx b/frontend/src/components/pages/rp-connect/Pipelines.List.tsx index 6d3930fb6..3b3a55c65 100644 --- a/frontend/src/components/pages/rp-connect/Pipelines.List.tsx +++ b/frontend/src/components/pages/rp-connect/Pipelines.List.tsx @@ -135,7 +135,7 @@ class RpConnectPipelinesList extends PageComponent<{}> { {/* Pipeline List */} {pipelinesApi.pipelines.length != 0 && ( - + this.refreshData(true); } - refreshData(_force: boolean) { - rpcnSecretManagerApi.refreshSecrets(_force); + refreshData(force: boolean) { + rpcnSecretManagerApi.refreshSecrets(force); } cancel() { @@ -77,9 +77,9 @@ class RpConnectSecretCreate extends PageComponent { render() { if (!rpcnSecretManagerApi.secrets) return DefaultSkeleton; - const alreadyExists = (rpcnSecretManagerApi.secrets || []).any(x => x.id == this.id); - const isIdEmpty = this.id.trim().length == 0; - const isSecretEmpty = this.secret.trim().length == 0; + const alreadyExists = (rpcnSecretManagerApi.secrets || []).any(x => x.id === this.id); + const isIdEmpty = this.id.trim().length === 0; + const isSecretEmpty = this.secret.trim().length === 0; return ( diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx index 37753c88d..72830e6d5 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.List.tsx @@ -147,7 +147,7 @@ class RpConnectSecretsList extends PageComponent { onClick={e => { e.stopPropagation(); e.preventDefault(); - appGlobal.history.push(`/rp-connect/secret/${r.id}/edit`); + appGlobal.history.push(`/rp-connect/secrets/${r.id}/edit`); }}> diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx index 5b7702f8e..f7ccb1b21 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.Update.tsx @@ -25,7 +25,7 @@ class RpConnectSecretUpdate extends PageComponent<{ secretId: string }> { initPage(p: PageInitHelper) { p.title = 'Update Secret'; - p.addBreadcrumb('Redpanda Connect Secret Manager', '/rp-connect/secret/update'); + p.addBreadcrumb('Redpanda Connect Secret Manager', '/rp-connect/secrets/update'); p.addBreadcrumb('Update Secret', ''); this.refreshData(true); diff --git a/frontend/src/components/routes.tsx b/frontend/src/components/routes.tsx index e7c8bc23e..4b3dd23ee 100644 --- a/frontend/src/components/routes.tsx +++ b/frontend/src/components/routes.tsx @@ -317,11 +317,11 @@ export const APP_ROUTES: IRouteEntry[] = [ MakeRoute<{ transformName: string }>('/transforms/:transformName', TransformDetails, 'Transforms'), // MakeRoute<{}>('/rp-connect', RpConnectPipelinesList, 'Connectors', LinkIcon, true), - MakeRoute<{}>('/rp-connect/secret/create', RpConnectSecretCreate, 'Connector-Secrets'), + MakeRoute<{}>('/rp-connect/secrets/create', RpConnectSecretCreate, 'Connector-Secrets'), MakeRoute<{}>('/rp-connect/create', RpConnectPipelinesCreate, 'Connectors'), MakeRoute<{ pipelineId: string }>('/rp-connect/:pipelineId', RpConnectPipelinesDetails, 'Connectors'), MakeRoute<{ pipelineId: string }>('/rp-connect/:pipelineId/edit', RpConnectPipelinesEdit, 'Connectors'), - MakeRoute<{ secretId: string }>('/rp-connect/secret/:secretId/edit', RpConnectSecretUpdate, 'Connector-Secrets'), + MakeRoute<{ secretId: string }>('/rp-connect/secrets/:secretId/edit', RpConnectSecretUpdate, 'Connector-Secrets'), MakeRoute<{}>('/reassign-partitions', ReassignPartitions, 'Reassign Partitions', BeakerIcon, false, routeVisibility(true, From a30244fb57534e58e37f2ae7fcbd1ea4684005cd Mon Sep 17 00:00:00 2001 From: Andres Aristizabal Date: Thu, 28 Nov 2024 07:21:23 -0500 Subject: [PATCH 19/19] rpcn-secret: add manual validation for secret name --- .../rp-connect/secrets/Secrets.Create.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx b/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx index ce55e49f4..2de41fee4 100644 --- a/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx +++ b/frontend/src/components/pages/rp-connect/secrets/Secrets.Create.tsx @@ -4,7 +4,7 @@ import {observer} from 'mobx-react'; import {appGlobal} from '../../../../state/appGlobal'; import {pipelinesApi, rpcnSecretManagerApi} from '../../../../state/backendApi'; import PageContent from '../../../misc/PageContent'; -import {action, makeObservable, observable} from 'mobx'; +import {action, computed, makeObservable, observable} from 'mobx'; import {DefaultSkeleton} from '../../../../utils/tsxUtils'; import {formatPipelineError} from '../errors'; import {CreateSecretRequest, Scope} from '../../../../protogen/redpanda/api/dataplane/v1alpha2/secret_pb'; @@ -74,10 +74,26 @@ class RpConnectSecretCreate extends PageComponent { }); } + @computed + get isNameValid() { + if ((rpcnSecretManagerApi.secrets || []).any(x => x.id === this.id)) { + return 'Secret name is already in use'; + } + if (this.id === '') { + return ''; + } + if (!(/^[A-Z][A-Z0-9_]*$/.test(this.id))) { + return 'The name you entered is invalid. It must start with an uppercase letter (A–Z) and can only contain uppercase letters (A–Z), digits (0–9), and underscores (_).'; + } + if (this.id.length > 255) { + return 'The secret name must be fewer than 255 characters.'; + } + return ''; + } + render() { if (!rpcnSecretManagerApi.secrets) return DefaultSkeleton; - const alreadyExists = (rpcnSecretManagerApi.secrets || []).any(x => x.id === this.id); const isIdEmpty = this.id.trim().length === 0; const isSecretEmpty = this.secret.trim().length === 0; @@ -85,7 +101,7 @@ class RpConnectSecretCreate extends PageComponent { - + -