From 698732441789ef1a678d8e12b610cb70880d934c Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 1 Dec 2023 16:45:15 +0400 Subject: [PATCH 1/3] #RI-5133 - update pipeline endpoint --- .../api/src/modules/rdi/client/api.rdi.client.ts | 3 ++- redisinsight/api/src/modules/rdi/constants/index.ts | 1 + .../api/src/modules/rdi/rdi-pipeline.controller.ts | 13 ++++++++++++- .../api/src/modules/rdi/rdi-pipeline.service.ts | 8 +++++--- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts index 60e08697cd..6e3f335e17 100644 --- a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts +++ b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts @@ -19,7 +19,8 @@ export class ApiRdiClient extends RdiClient { } async getPipeline(): Promise { - return null; + const response = await this.client.get(RdiUrl.GetPipeline); + return response.data; } async deploy(pipeline: RdiPipeline): Promise { diff --git a/redisinsight/api/src/modules/rdi/constants/index.ts b/redisinsight/api/src/modules/rdi/constants/index.ts index 68796fecb0..3c888a8a90 100644 --- a/redisinsight/api/src/modules/rdi/constants/index.ts +++ b/redisinsight/api/src/modules/rdi/constants/index.ts @@ -1,3 +1,4 @@ export enum RdiUrl { GetSchema = '/schema', + GetPipeline = '/pipeline', } diff --git a/redisinsight/api/src/modules/rdi/rdi-pipeline.controller.ts b/redisinsight/api/src/modules/rdi/rdi-pipeline.controller.ts index 0b00813a30..3783ea8ec9 100644 --- a/redisinsight/api/src/modules/rdi/rdi-pipeline.controller.ts +++ b/redisinsight/api/src/modules/rdi/rdi-pipeline.controller.ts @@ -1,7 +1,7 @@ import { ClassSerializerInterceptor, Controller, Get, UseInterceptors, UsePipes, ValidationPipe, } from '@nestjs/common'; -import { Rdi, RdiClientMetadata } from 'src/modules/rdi/models'; +import { Rdi, RdiPipeline, RdiClientMetadata } from 'src/modules/rdi/models'; import { ApiTags } from '@nestjs/swagger'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { RdiPipelineService } from 'src/modules/rdi/rdi-pipeline.service'; @@ -26,4 +26,15 @@ export class RdiPipelineController { ): Promise { return this.rdiPipelineService.getSchema(rdiClientMetadata); } + + @Get('/') + @ApiEndpoint({ + description: 'Get pipeline', + responses: [{ status: 200, type: RdiPipeline }], + }) + async getPipeline( + @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata, + ): Promise { + return this.rdiPipelineService.getPipeline(rdiClientMetadata); + } } diff --git a/redisinsight/api/src/modules/rdi/rdi-pipeline.service.ts b/redisinsight/api/src/modules/rdi/rdi-pipeline.service.ts index 05813639cb..85203c8d79 100644 --- a/redisinsight/api/src/modules/rdi/rdi-pipeline.service.ts +++ b/redisinsight/api/src/modules/rdi/rdi-pipeline.service.ts @@ -11,10 +11,12 @@ export class RdiPipelineService { async getSchema(rdiClientMetadata: RdiClientMetadata): Promise { const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata); - const schema = await client.getSchema(); + return await client.getSchema(); + } - // todo: process somehow + async getPipeline(rdiClientMetadata: RdiClientMetadata): Promise { + const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata); - return schema; + return await client.getPipeline(); } } From 2ef8d5627f6e7e1237067f5d8fb2972cb333e227 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 1 Dec 2023 17:36:19 +0400 Subject: [PATCH 2/3] #RI-5133 - add rdi pipeline config --- redisinsight/ui/src/assets/img/icons/file.svg | 10 ++ .../main-router/constants/defaultRoutes.ts | 8 +- .../constants/sub-routes/rdiRoutes.ts | 18 +-- .../monaco-yaml/MonacoYaml.spec.tsx | 10 ++ .../components/monaco-yaml/MonacoYaml.tsx | 32 ++++ .../components/monaco-yaml/index.ts | 3 + .../ui/src/components/monaco-editor/index.ts | 2 + redisinsight/ui/src/constants/api.ts | 2 + redisinsight/ui/src/constants/links.ts | 1 + redisinsight/ui/src/constants/pages.ts | 3 + .../pages/rdi/pipeline/PipelinePage.spec.tsx | 45 ++++++ .../src/pages/rdi/pipeline/PipelinePage.tsx | 35 +++++ .../rdi/pipeline/PipelinePageRouter.spec.tsx | 17 ++ .../pages/rdi/pipeline/PipelinePageRouter.tsx | 17 ++ .../components/navigation/Navigation.spec.tsx | 36 +++++ .../components/navigation/Navigation.tsx | 88 +++++++++++ .../pipeline/components/navigation/index.ts | 3 + .../components/navigation/styles.module.scss | 78 ++++++++++ .../ui/src/pages/rdi/pipeline/index.ts | 3 + .../rdi/pipeline/pages/config/Config.spec.tsx | 48 ++++++ .../rdi/pipeline/pages/config/Config.tsx | 73 +++++++++ .../pages/rdi/pipeline/pages/config/index.ts | 3 + .../pipeline/pages/config/styles.module.scss | 33 ++++ .../rdi/pipeline/pages/prepare/Prepare.tsx | 14 ++ .../pages/rdi/pipeline/pages/prepare/index.ts | 3 + .../src/pages/rdi/pipeline/styles.module.scss | 21 +++ redisinsight/ui/src/slices/interfaces/rdi.ts | 12 ++ redisinsight/ui/src/slices/rdi/pipeline.ts | 73 +++++++++ redisinsight/ui/src/slices/store.ts | 4 +- .../ui/src/slices/tests/rdi/pipeline.spec.ts | 147 ++++++++++++++++++ redisinsight/ui/src/telemetry/pageViews.ts | 3 +- 31 files changed, 830 insertions(+), 15 deletions(-) create mode 100644 redisinsight/ui/src/assets/img/icons/file.svg create mode 100644 redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.spec.tsx create mode 100644 redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.tsx create mode 100644 redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/index.ts create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/PipelinePage.spec.tsx create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/PipelinePage.tsx create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/PipelinePageRouter.spec.tsx create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/PipelinePageRouter.tsx create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.spec.tsx create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.tsx create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/components/navigation/index.ts create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/components/navigation/styles.module.scss create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/index.ts create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.spec.tsx create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.tsx create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/pages/config/index.ts create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/pages/config/styles.module.scss create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/Prepare.tsx create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/index.ts create mode 100644 redisinsight/ui/src/pages/rdi/pipeline/styles.module.scss create mode 100644 redisinsight/ui/src/slices/interfaces/rdi.ts create mode 100644 redisinsight/ui/src/slices/rdi/pipeline.ts create mode 100644 redisinsight/ui/src/slices/tests/rdi/pipeline.spec.ts diff --git a/redisinsight/ui/src/assets/img/icons/file.svg b/redisinsight/ui/src/assets/img/icons/file.svg new file mode 100644 index 0000000000..1d0a119992 --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/file.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts index c997876761..7b358d10b6 100644 --- a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts @@ -13,7 +13,8 @@ import WorkbenchPage from 'uiSrc/pages/workbench' import PubSubPage from 'uiSrc/pages/pub-sub' import AnalyticsPage from 'uiSrc/pages/analytics' import TriggeredFunctionsPage from 'uiSrc/pages/triggered-functions' -import RdiList from 'uiSrc/pages/rdi/home' +import RdiList from 'uiSrc/pages/rdi/home/RDIList' +import RdiPipeline from 'uiSrc/pages/rdi/pipeline/PipelinePage' import { ANALYTICS_ROUTES, RDI_ROUTES, TRIGGERED_FUNCTIONS_ROUTES } from './sub-routes' import COMMON_ROUTES from './commonRoutes' @@ -77,9 +78,8 @@ const ROUTES: IRoute[] = [ ], }, { - path: Pages.rdi, - // todo: add home rdi component - list of instances - component: RdiList, + path: '/integrate/:rdiInstanceId', + component: RdiPipeline, routes: RDI_ROUTES, }, { diff --git a/redisinsight/ui/src/components/main-router/constants/sub-routes/rdiRoutes.ts b/redisinsight/ui/src/components/main-router/constants/sub-routes/rdiRoutes.ts index 4590df87e1..4a691b8a4f 100644 --- a/redisinsight/ui/src/components/main-router/constants/sub-routes/rdiRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/sub-routes/rdiRoutes.ts @@ -1,14 +1,14 @@ -import { IRoute } from 'uiSrc/constants' -import RdiList from 'uiSrc/pages/rdi/home' +import { IRoute, Pages } from 'uiSrc/constants' +import PreparePage from 'uiSrc/pages/rdi/pipeline/pages/prepare' +import ConfigPage from 'uiSrc/pages/rdi/pipeline/pages/config' export const RDI_ROUTES: IRoute[] = [ { - // todo: rename path - path: '/:rdiId', - // todo add component like Instance page - component: RdiList, - routes: [ - // todo: add page routes here - ], + path: Pages.rdiPipelinePrepare(':rdiInstanceId'), + component: PreparePage, + }, + { + path: Pages.rdiPipelineConfig(':rdiInstanceId'), + component: ConfigPage, }, ] diff --git a/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.spec.tsx b/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.spec.tsx new file mode 100644 index 0000000000..30c5684f3d --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { render } from 'uiSrc/utils/test-utils' + +import MonacoYaml from './MonacoYaml' + +describe('MonacoYaml', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.tsx b/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.tsx new file mode 100644 index 0000000000..6b8b948e82 --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api' + +import { MonacoEditor } from 'uiSrc/components/monaco-editor' +import { CommonProps } from 'uiSrc/components/monaco-editor/MonacoEditor' + +const MonacoYaml = (props: CommonProps) => { + const editorDidMount = ( + editor: monacoEditor.editor.IStandaloneCodeEditor, + monaco: typeof monacoEditor, + ) => { + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + schemaValidation: 'error', + schemaRequest: 'error', + trailingCommas: 'error' + }) + const messageContribution = editor.getContribution('editor.contrib.messageController') + editor.onDidAttemptReadOnlyEdit(() => messageContribution.dispose()) + } + + return ( + + ) +} + +export default MonacoYaml diff --git a/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/index.ts b/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/index.ts new file mode 100644 index 0000000000..f5cf5dbd17 --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/index.ts @@ -0,0 +1,3 @@ +import MonacoYaml from './MonacoYaml' + +export default MonacoYaml diff --git a/redisinsight/ui/src/components/monaco-editor/index.ts b/redisinsight/ui/src/components/monaco-editor/index.ts index cbd6f985f9..38b43c4995 100644 --- a/redisinsight/ui/src/components/monaco-editor/index.ts +++ b/redisinsight/ui/src/components/monaco-editor/index.ts @@ -1,9 +1,11 @@ import MonacoEditor from './MonacoEditor' import MonacoJS from './components/monaco-js' import MonacoJson from './components/monaco-json' +import MonacoYaml from './components/monaco-yaml' export { MonacoEditor, MonacoJS, MonacoJson, + MonacoYaml, } diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index 6351c824b2..e6c1b4d532 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -136,6 +136,8 @@ enum ApiEndpoints { ANALYTICS_SEND_EVENT = 'analytics/send-event', ANALYTICS_SEND_PAGE = 'analytics/send-page', + + RDI_PIPELINE = 'rdi/pipeline' } export enum CustomHeaders { diff --git a/redisinsight/ui/src/constants/links.ts b/redisinsight/ui/src/constants/links.ts index e92975c40d..b68c303ebe 100644 --- a/redisinsight/ui/src/constants/links.ts +++ b/redisinsight/ui/src/constants/links.ts @@ -11,6 +11,7 @@ export const EXTERNAL_LINKS = { cloudConsole: 'https://app.redislabs.com/#/databases', tryFree: 'https://redis.com/try-free', docker: 'https://redis.io/docs/getting-started/install-stack/docker', + rdiQuickStart: 'https://docs.redis.com/latest/rdi/quickstart/', } export const UTM_CAMPAINGS: Record = { diff --git a/redisinsight/ui/src/constants/pages.ts b/redisinsight/ui/src/constants/pages.ts index 41c3ef03f7..d230d696bc 100644 --- a/redisinsight/ui/src/constants/pages.ts +++ b/redisinsight/ui/src/constants/pages.ts @@ -51,4 +51,7 @@ export const Pages = { `/${instanceId}/${PageNames.triggeredFunctions}/${PageNames.triggeredFunctionsFunctions}`, // rdi pages rdi: '/integrate', + rdiPipeline: (rdiInstance: string) => `integrate/${rdiInstance}/pipeline}`, + rdiPipelineConfig: (rdiInstance: string) => `/integrate/${rdiInstance}/pipeline/config`, + rdiPipelinePrepare: (rdiInstance: string) => `/integrate/${rdiInstance}/pipeline/prepare`, } diff --git a/redisinsight/ui/src/pages/rdi/pipeline/PipelinePage.spec.tsx b/redisinsight/ui/src/pages/rdi/pipeline/PipelinePage.spec.tsx new file mode 100644 index 0000000000..48f839586d --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/PipelinePage.spec.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import { cloneDeep } from 'lodash' + +import { BrowserRouter } from 'react-router-dom' +import { instance, mock } from 'ts-mockito' +import { act, render, cleanup, mockedStore } from 'uiSrc/utils/test-utils' +import { getPipeline } from 'uiSrc/slices/rdi/pipeline' +import PipelinePage, { Props } from './PipelinePage' + +const mockedProps = mock() + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('PipelinePage', () => { + it('should render', () => { + expect( + render( + + + + ) + ).toBeTruthy() + }) + + it('should dispatch fetchRdiPipeline on render', async () => { + await act(() => { + render( + + + + ) + }) + + const expectedActions = [ + getPipeline(), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) +}) diff --git a/redisinsight/ui/src/pages/rdi/pipeline/PipelinePage.tsx b/redisinsight/ui/src/pages/rdi/pipeline/PipelinePage.tsx new file mode 100644 index 0000000000..c08c20ada1 --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/PipelinePage.tsx @@ -0,0 +1,35 @@ +import React, { useEffect } from 'react' +import { useLocation, useParams } from 'react-router-dom' +import { useDispatch } from 'react-redux' + +import { fetchRdiPipeline } from 'uiSrc/slices/rdi/pipeline' + +import Navigation from './components/navigation' +import PipelinePageRouter from './PipelinePageRouter' +import styles from './styles.module.scss' + +export interface Props { + routes: any[] +} + +const Pipeline = ({ routes = [] }: Props) => { + const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>() + + const { pathname } = useLocation() + const dispatch = useDispatch() + + const path = pathname?.split('/').pop() || '' + + useEffect(() => { + dispatch(fetchRdiPipeline(rdiInstanceId)) + }, []) + + return ( +
+ + +
+ ) +} + +export default Pipeline diff --git a/redisinsight/ui/src/pages/rdi/pipeline/PipelinePageRouter.spec.tsx b/redisinsight/ui/src/pages/rdi/pipeline/PipelinePageRouter.spec.tsx new file mode 100644 index 0000000000..8d3e1b6d01 --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/PipelinePageRouter.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { render } from 'uiSrc/utils/test-utils' +import PipelinePageRouter from './PipelinePageRouter' + +const mockedRoutes = [ + { + path: '/page', + }, +] + +describe('PipelinePageRouter', () => { + it('should render', () => { + expect( + render(, { withRouter: true }) + ).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/rdi/pipeline/PipelinePageRouter.tsx b/redisinsight/ui/src/pages/rdi/pipeline/PipelinePageRouter.tsx new file mode 100644 index 0000000000..63246baf23 --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/PipelinePageRouter.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { Switch } from 'react-router-dom' +import RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes' + +export interface Props { + routes: any[]; +} +const PipelinePageRouter = ({ routes }: Props) => ( + + {routes.map((route, i) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + +) + +export default React.memo(PipelinePageRouter) diff --git a/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.spec.tsx b/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.spec.tsx new file mode 100644 index 0000000000..3a9c598cce --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import reactRouterDom from 'react-router-dom' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' + +import Navigation from './Navigation' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: jest.fn, + }), +})) + +describe('Navigation', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call proper history push after click on tabs', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + fireEvent.click(screen.getByTestId('rdi-nav-btn-config')) + expect(pushMock).toBeCalledWith('/integrate/undefined/pipeline/config') + + fireEvent.click(screen.getByTestId('rdi-nav-btn-prepare')) + expect(pushMock).toBeCalledWith('/integrate/undefined/pipeline/prepare') + }) +}) diff --git a/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.tsx b/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.tsx new file mode 100644 index 0000000000..10887905e3 --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import { useHistory, useParams } from 'react-router-dom' +import { + EuiTextColor, + EuiText, + EuiTab, + EuiTabs, + EuiIcon, +} from '@elastic/eui' +import cx from 'classnames' + +import { Pages } from 'uiSrc/constants' +import { ReactComponent as FileIcon } from 'uiSrc/assets/img/icons/file.svg' + +import styles from './styles.module.scss' + +export interface IProps { + path: string +} + +enum RdiPipelineNav { + Prepare = 'prepare', + Config = 'config', +} + +const defaultNavList = [ + { + id: RdiPipelineNav.Prepare, + title: 'Prepare', + fileName: 'Select pipeline type', + }, + { + id: RdiPipelineNav.Config, + title: 'Configuration', + fileName: 'Target connection details' + } +] + +const Navigation = (props: IProps) => { + const { path } = props + + const history = useHistory() + + const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>() + + // TODO resolve job id + const onSelectedTabChanged = (id: string) => { + if (id === RdiPipelineNav.Prepare) { + history.push(Pages.rdiPipelinePrepare(rdiInstanceId)) + } + if (id === RdiPipelineNav.Config) { + history.push(Pages.rdiPipelineConfig(rdiInstanceId)) + } + } + + const renderTabs = () => defaultNavList.map(({ id, title, fileName }) => ( + + <> + {title} +
{}} + onClick={() => onSelectedTabChanged(id)} + data-testid={`rdi-nav-btn-${id}`} + > + + {fileName} +
+ +
+ )) + + return ( +
+ Pipeline Management + {renderTabs()} +
+ ) +} + +export default Navigation diff --git a/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/index.ts b/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/index.ts new file mode 100644 index 0000000000..d4dd76dd61 --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/index.ts @@ -0,0 +1,3 @@ +import Navigation from './Navigation' + +export default Navigation diff --git a/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/styles.module.scss b/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/styles.module.scss new file mode 100644 index 0000000000..baae24f07b --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/styles.module.scss @@ -0,0 +1,78 @@ +.wrapper { + border-radius: 8px; + width: 269px; + background-color: var(--euiColorLightestShade); + height: 100%; + + .title { + padding: 16px; + font: normal normal 500 14px/17px Graphik, sans-serif; + border-bottom: 1px solid var(--separatorColor); + } + + .tabs { + flex-direction: column; + + .tab { + cursor: default; + border-bottom: 1px solid var(--separatorColor) !important; + + &:global(.euiTab-isSelected) { + background-color: var(--tableRowSelectedColor) !important; + border-left: 2px solid var(--euiColorPrimary); + + .tabTitle { + font-weight: 500; + } + + .text { + color: var(--iconsDefaultHoverColor) !important; + } + + .fileIcon path { + fill: var(--iconsDefaultHoverColor); + } + } + + &:global(.euiTab:hover:not(.euiTab-isSelected)) { + text-decoration: none; + + .text:hover { + text-decoration: underline; + } + } + + :global(.euiTab__content) { + padding: 16px 12px 16px 16px; + } + + .tabTitle { + font: normal normal normal 14px/17px Graphik, sans-serif; + color: var(--externalLinkColor) !important; + text-align: left; + padding: 3px 0; + } + + .file { + display: flex; + padding: 3px 0; + } + + .text { + font: normal normal normal 14px/17px Graphik, sans-serif; + color: var(--euiTextSubduedColor) !important; + } + + .fileIcon { + width: 14px; + height: 15px; + margin-right: 6px; + + path { + fill: var(--euiTextSubduedColor); + } + } + } + } +} + diff --git a/redisinsight/ui/src/pages/rdi/pipeline/index.ts b/redisinsight/ui/src/pages/rdi/pipeline/index.ts new file mode 100644 index 0000000000..e21e78dd0c --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/index.ts @@ -0,0 +1,3 @@ +import PipelinePage from './PipelinePage' + +export default PipelinePage diff --git a/redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.spec.tsx b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.spec.tsx new file mode 100644 index 0000000000..472c8cf27b --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.spec.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline' +import { render, screen } from 'uiSrc/utils/test-utils' + +import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' +import Config from './Config' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendPageViewTelemetry: jest.fn(), +})) + +jest.mock('uiSrc/slices/rdi/pipeline', () => ({ + ...jest.requireActual('uiSrc/slices/rdi/pipeline'), + rdiPipelineSelector: jest.fn().mockReturnValue({ + loading: false, + error: '', + data: null, + }), +})) + +describe('Config', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call proper sendPageViewTelemetry', () => { + const sendPageViewTelemetryMock = jest.fn() + sendPageViewTelemetry.mockImplementation(() => sendPageViewTelemetryMock) + + render() + + expect(sendPageViewTelemetry).toBeCalledWith({ + name: TelemetryPageView.RDI_CONFIG, + }) + }) + + it('should render loading spinner', () => { + const rdiPipelineSelectorMock = jest.fn().mockReturnValue({ + loading: true, + }) + rdiPipelineSelector.mockImplementation(rdiPipelineSelectorMock) + + render() + + expect(screen.getByTestId('rdi-config-loading')).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.tsx b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.tsx new file mode 100644 index 0000000000..03096bd8cc --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.tsx @@ -0,0 +1,73 @@ +import React, { useState, useEffect } from 'react' +import { useSelector } from 'react-redux' +import { EuiText, EuiLink, EuiButton, EuiLoadingSpinner } from '@elastic/eui' +import cx from 'classnames' + +import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' +import { EXTERNAL_LINKS } from 'uiSrc/constants/links' +import { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline' +import { MonacoYaml } from 'uiSrc/components/monaco-editor' + +import styles from './styles.module.scss' + +const Content = () => { + const { loading, data } = useSelector(rdiPipelineSelector) + + const [value, setValue] = useState(data?.config ?? '') + + useEffect(() => { + setValue(data?.config ?? '') + }, [data]) + + useEffect(() => { + sendPageViewTelemetry({ + name: TelemetryPageView.RDI_CONFIG, + }) + }, []) + + return ( +
+ Target database configuration + + {'Configure target instance '} + + connection details + + {' and applier settings.'} + + {loading ? ( +
+ Loading data... + +
+ ) : ( + + )} + +
+ {}} + data-testid="rdi-test-connection" + > + Test Connection + +
+
+ ) +} + +export default Content diff --git a/redisinsight/ui/src/pages/rdi/pipeline/pages/config/index.ts b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/index.ts new file mode 100644 index 0000000000..1fca4da459 --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/index.ts @@ -0,0 +1,3 @@ +import Config from './Config' + +export default Config diff --git a/redisinsight/ui/src/pages/rdi/pipeline/pages/config/styles.module.scss b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/styles.module.scss new file mode 100644 index 0000000000..1c3b72a34b --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/styles.module.scss @@ -0,0 +1,33 @@ +.wrapper { + .title { + font: normal normal normal 16px/19px Graphik, sans-serif; + } + + .title, + .text { + margin-bottom: 8px; + } + + .actions { + display: flex; + justify-content: end; + padding-top: 16px; + } + + .editorWrapper { + height: calc(100% - 105px); + border: 1px solid var(--separatorColorLight) !important; + + :global(.inlineMonacoEditor) { + height: 100%; + } + } + + .loading { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: var(--browserViewTypePassive) !important; + } +} \ No newline at end of file diff --git a/redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/Prepare.tsx b/redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/Prepare.tsx new file mode 100644 index 0000000000..90fff831b8 --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/Prepare.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { EuiTextColor } from '@elastic/eui' + +export interface IProps { + +} + +const Content = () => ( +
+ prepare +
+) + +export default Content diff --git a/redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/index.ts b/redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/index.ts new file mode 100644 index 0000000000..f4e7b67a8d --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/index.ts @@ -0,0 +1,3 @@ +import Prepare from './Prepare' + +export default Prepare diff --git a/redisinsight/ui/src/pages/rdi/pipeline/styles.module.scss b/redisinsight/ui/src/pages/rdi/pipeline/styles.module.scss new file mode 100644 index 0000000000..b232dc9519 --- /dev/null +++ b/redisinsight/ui/src/pages/rdi/pipeline/styles.module.scss @@ -0,0 +1,21 @@ +.wrapper { + display: flex; + width: 100%; + height: 100%; + padding: 16px; + + :global(.content) { + border-radius: 8px; + padding: 32px 24px 16px 24px; + margin-left: 16px; + background-color: var(--euiColorEmptyShade); + width: 100%; + height: 100%; + } + + :global { + .monaco-editor, .monaco-editor .margin, .monaco-editor .minimap-decorations-layer, .monaco-editor-background { + background-color: var(--browserViewTypePassive) !important; + } + } +} \ No newline at end of file diff --git a/redisinsight/ui/src/slices/interfaces/rdi.ts b/redisinsight/ui/src/slices/interfaces/rdi.ts new file mode 100644 index 0000000000..c3d7fe45b5 --- /dev/null +++ b/redisinsight/ui/src/slices/interfaces/rdi.ts @@ -0,0 +1,12 @@ +import { Nullable } from 'uiSrc/utils' + +export interface IPipeline { + config: string + jobs: any[] +} + +export interface IStateRdi { + loading: boolean; + error: string + data: Nullable +} diff --git a/redisinsight/ui/src/slices/rdi/pipeline.ts b/redisinsight/ui/src/slices/rdi/pipeline.ts new file mode 100644 index 0000000000..704ff542d4 --- /dev/null +++ b/redisinsight/ui/src/slices/rdi/pipeline.ts @@ -0,0 +1,73 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { AxiosError } from 'axios' +import { apiService, } from 'uiSrc/services' +import { addErrorNotification } from 'uiSrc/slices/app/notifications' +import { IStateRdi, IPipeline } from 'uiSrc/slices/interfaces/rdi' +import { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils' + +import { AppDispatch, RootState } from '../store' + +export const initialState: IStateRdi = { + loading: false, + error: '', + data: null, +} + +const rdiPipelineSlice = createSlice({ + name: 'rdiPipeline', + initialState, + reducers: { + getPipeline: (state) => { + state.loading = true + }, + getPipelineSuccess: (state, { payload }: PayloadAction) => { + state.loading = false + state.data = payload + }, + getPipelineFailure: (state, { payload }) => { + state.loading = false + state.error = payload + }, + } +}) + +export const rdiSelector = (state: RootState) => state.rdi +export const rdiPipelineSelector = (state: RootState) => state.rdi.pipeline + +export const { + getPipeline, + getPipelineSuccess, + getPipelineFailure, +} = rdiPipelineSlice.actions + +// The reducer +export default rdiPipelineSlice.reducer + +// Asynchronous thunk action +export function fetchRdiPipeline( + rdiInstanceId: string, + onSuccessAction?: (data: any) => void, + onFailAction?: () => void, +) { + return async (dispatch: AppDispatch) => { + try { + dispatch(getPipeline()) + const { data, status } = await apiService.get( + // TODO connect with Kyle to find solution + `rdi/${rdiInstanceId}/pipeline` + ) + + if (isStatusSuccessful(status)) { + dispatch(getPipelineSuccess(data)) + + onSuccessAction?.(data) + } + } catch (_err) { + const error = _err as AxiosError + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + dispatch(getPipelineFailure(errorMessage)) + onFailAction?.() + } + } +} diff --git a/redisinsight/ui/src/slices/store.ts b/redisinsight/ui/src/slices/store.ts index e96ae89903..f64c7173b6 100644 --- a/redisinsight/ui/src/slices/store.ts +++ b/redisinsight/ui/src/slices/store.ts @@ -45,6 +45,7 @@ import recommendationsReducer from './recommendations/recommendations' import triggeredFunctionsReducer from './triggeredFunctions/triggeredFunctions' import insightsPanelReducer from './panels/insights' import appRDIReducer from './rdi/rdi' +import rdiPipelineSlice from './rdi/pipeline' export const history = createBrowserHistory() @@ -113,7 +114,8 @@ export const rootReducer = combineReducers({ insights: insightsPanelReducer, }), rdi: combineReducers({ - rdi: appRDIReducer + rdi: appRDIReducer, + pipeline: rdiPipelineSlice, }) }) diff --git a/redisinsight/ui/src/slices/tests/rdi/pipeline.spec.ts b/redisinsight/ui/src/slices/tests/rdi/pipeline.spec.ts new file mode 100644 index 0000000000..f6416608f4 --- /dev/null +++ b/redisinsight/ui/src/slices/tests/rdi/pipeline.spec.ts @@ -0,0 +1,147 @@ +import { cloneDeep } from 'lodash' +import { AxiosError } from 'axios' +import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' +import reducer, { + initialState, + getPipeline, + getPipelineSuccess, + getPipelineFailure, + fetchRdiPipeline, + rdiSelector, +} from 'uiSrc/slices/rdi/pipeline' +import { apiService } from 'uiSrc/services' +import { addErrorNotification } from 'uiSrc/slices/app/notifications' + +let store: typeof mockedStore + +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('rdi pipe slice', () => { + describe('reducer, actions and selectors', () => { + it('should return the initial state', () => { + // Arrange + const nextState = initialState + + // Act + const result = reducer(undefined, {}) + + // Assert + expect(result).toEqual(nextState) + }) + }) + + describe('rdiSelector', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + loading: true, + } + + // Act + const nextState = reducer(initialState, getPipeline()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + rdi: nextState, + }) + expect(rdiSelector(rootState)).toEqual(state) + }) + }) + + describe('getPipelineSuccess', () => { + it('should properly set state', () => { + // Arrange + const pipeline = { config: 'string', jobs: [] } + const state = { + ...initialState, + data: pipeline, + } + + // Act + const nextState = reducer(initialState, getPipelineSuccess(pipeline)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + rdi: nextState, + }) + expect(rdiSelector(rootState)).toEqual(state) + }) + }) + + describe('getPipelineFailure', () => { + it('should properly set state', () => { + // Arrange + const error = 'error' + const state = { + ...initialState, + error, + } + + // Act + const nextState = reducer(initialState, getPipelineFailure(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + rdi: nextState, + }) + expect(rdiSelector(rootState)).toEqual(state) + }) + }) + + // thunks + + describe('thunks', () => { + describe('fetchRdiPipeline', () => { + it('succeed to fetch data', async () => { + const data = { config: 'string', jobs: [] } + const responsePayload = { data, status: 200 } + + apiService.get = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch( + fetchRdiPipeline('123') + ) + + // Assert + const expectedActions = [ + getPipeline(), + getPipelineSuccess(data), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch data', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.get = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch( + fetchRdiPipeline('123') + ) + + // Assert + const expectedActions = [ + getPipeline(), + addErrorNotification(responsePayload as AxiosError), + getPipelineFailure(errorMessage) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) + }) +}) diff --git a/redisinsight/ui/src/telemetry/pageViews.ts b/redisinsight/ui/src/telemetry/pageViews.ts index 7515f2af01..f799900dfd 100644 --- a/redisinsight/ui/src/telemetry/pageViews.ts +++ b/redisinsight/ui/src/telemetry/pageViews.ts @@ -8,5 +8,6 @@ export enum TelemetryPageView { CLUSTER_DETAILS_PAGE = 'Overview', PUBSUB_PAGE = 'Pub/Sub', DATABASE_ANALYSIS = 'Database Analysis', - TRIGGERED_FUNCTIONS = 'Triggers and Functions' + TRIGGERED_FUNCTIONS = 'Triggers and Functions', + RDI_CONFIG = 'RDI Configuration' } From 0ccefbd3feed2e60696a76b4b980ce3b332c37c4 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 4 Dec 2023 12:37:55 +0400 Subject: [PATCH 3/3] #RI-5133 -resolve comments --- .../monaco-editor/components/monaco-yaml/MonacoYaml.tsx | 2 +- .../pages/rdi/pipeline/components/navigation/Navigation.tsx | 2 +- .../ui/src/pages/rdi/pipeline/pages/config/Config.tsx | 4 ++-- .../src/pages/rdi/pipeline/pages/config/styles.module.scss | 2 +- .../ui/src/pages/rdi/pipeline/pages/prepare/Prepare.tsx | 5 +++-- redisinsight/ui/src/pages/rdi/pipeline/styles.module.scss | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.tsx b/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.tsx index 6b8b948e82..fed956f624 100644 --- a/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.tsx +++ b/redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.tsx @@ -23,7 +23,7 @@ const MonacoYaml = (props: CommonProps) => { ) diff --git a/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.tsx b/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.tsx index 10887905e3..b779b50611 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline/components/navigation/Navigation.tsx @@ -58,7 +58,7 @@ const Navigation = (props: IProps) => { isSelected={path === id} key={id} className={styles.tab} - data-testid={`triggered-functions-tab-${id}`} + data-testid={`rdi-pipeline-tab-${id}`} > <> {title} diff --git a/redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.tsx b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.tsx index 03096bd8cc..4588e0c038 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/Config.tsx @@ -10,7 +10,7 @@ import { MonacoYaml } from 'uiSrc/components/monaco-editor' import styles from './styles.module.scss' -const Content = () => { +const Config = () => { const { loading, data } = useSelector(rdiPipelineSelector) const [value, setValue] = useState(data?.config ?? '') @@ -70,4 +70,4 @@ const Content = () => { ) } -export default Content +export default Config diff --git a/redisinsight/ui/src/pages/rdi/pipeline/pages/config/styles.module.scss b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/styles.module.scss index 1c3b72a34b..17b6adc4c1 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline/pages/config/styles.module.scss +++ b/redisinsight/ui/src/pages/rdi/pipeline/pages/config/styles.module.scss @@ -30,4 +30,4 @@ align-items: center; background-color: var(--browserViewTypePassive) !important; } -} \ No newline at end of file +} diff --git a/redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/Prepare.tsx b/redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/Prepare.tsx index 90fff831b8..ddd300d011 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/Prepare.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline/pages/prepare/Prepare.tsx @@ -5,10 +5,11 @@ export interface IProps { } -const Content = () => ( +// TODO rename and update this component +const Prepare = () => (
prepare
) -export default Content +export default Prepare diff --git a/redisinsight/ui/src/pages/rdi/pipeline/styles.module.scss b/redisinsight/ui/src/pages/rdi/pipeline/styles.module.scss index b232dc9519..c0c26a850a 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline/styles.module.scss +++ b/redisinsight/ui/src/pages/rdi/pipeline/styles.module.scss @@ -18,4 +18,4 @@ background-color: var(--browserViewTypePassive) !important; } } -} \ No newline at end of file +}