From 6d33f3c3327ce586ce3178bd068a20b91353c466 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 17 Apr 2023 16:06:11 +0300 Subject: [PATCH 01/32] #RI-4403 upload data from tutorial --- .../bulk-actions/bulk-import.controller.ts | 18 ++++ .../bulk-actions/bulk-import.service.spec.ts | 69 ++++++++++++++- .../bulk-actions/bulk-import.service.ts | 35 +++++++- .../dto/upload-import-file-by-path.dto.ts | 12 +++ ...-bulk_actions-import-tutorial_data.test.ts | 86 +++++++++++++++++++ ...-databases-id-bulk_actions-import.test.ts} | 0 6 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 redisinsight/api/src/modules/bulk-actions/dto/upload-import-file-by-path.dto.ts create mode 100644 redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts rename redisinsight/api/test/api/bulk-actions/{POST-databases-id-bulk_actions-upload.test.ts => POST-databases-id-bulk_actions-import.test.ts} (100%) diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.controller.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.controller.ts index 00110f1792..98bb34715b 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.controller.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.controller.ts @@ -14,6 +14,7 @@ import { UploadImportFileDto } from 'src/modules/bulk-actions/dto/upload-import- import { ClientMetadataParam } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; import { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface'; +import { UploadImportFileByPathDto } from 'src/modules/bulk-actions/dto/upload-import-file-by-path.dto'; @UsePipes(new ValidationPipe({ transform: true })) @UseInterceptors(ClassSerializerInterceptor) @@ -40,4 +41,21 @@ export class BulkImportController { ): Promise { return this.service.import(clientMetadata, dto); } + + @Post('import/tutorial-data') + @HttpCode(200) + @ApiEndpoint({ + description: 'Import data from tutorial by path', + responses: [ + { + type: Object, + }, + ], + }) + async uploadFromTutorial( + @Body() dto: UploadImportFileByPathDto, + @ClientMetadataParam() clientMetadata: ClientMetadata, + ): Promise { + return this.service.uploadFromTutorial(clientMetadata, dto); + } } diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts index 4fb410217c..bffd669c7e 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts @@ -5,14 +5,19 @@ import { mockClientMetadata, mockDatabaseConnectionService, mockIORedisClient, - mockIORedisCluster, MockType + mockIORedisCluster, MockType, } from 'src/__mocks__'; import { MemoryStoredFile } from 'nestjs-form-data'; import { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-summary'; import { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface'; import { BulkActionStatus, BulkActionType } from 'src/modules/bulk-actions/constants'; -import { NotFoundException } from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; import { BulkActionsAnalyticsService } from 'src/modules/bulk-actions/bulk-actions-analytics.service'; +import * as fs from 'fs-extra'; +import config from 'src/utils/config'; +import { join } from 'path'; + +const PATH_CONFIG = config.get('dir_path'); const generateNCommandsBuffer = (n: number) => Buffer.from( (new Array(n)).fill(1).map(() => ['set', ['foo', 'bar']]).join('\n'), @@ -55,12 +60,20 @@ const mockUploadImportFileDto = { } as unknown as MemoryStoredFile, }; +const mockUploadImportFileByPathDto = { + path: '/some/path', +}; + +jest.mock('fs-extra'); +const mockedFs = fs as jest.Mocked; + describe('BulkImportService', () => { let service: BulkImportService; let databaseConnectionService: MockType; let analytics: MockType; beforeEach(async () => { + jest.mock('fs-extra', () => mockedFs); jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ @@ -186,4 +199,56 @@ describe('BulkImportService', () => { } }); }); + + describe('uploadFromTutorial', () => { + let spy; + + beforeEach(() => { + spy = jest.spyOn(service as any, 'import'); + spy.mockResolvedValue(mockSummary); + mockedFs.readFile.mockResolvedValue(Buffer.from('set foo bar')); + }); + + it('should import file by path', async () => { + mockedFs.pathExists.mockImplementationOnce(async () => true); + + await service.uploadFromTutorial(mockClientMetadata, mockUploadImportFileByPathDto); + + expect(mockedFs.readFile).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, mockUploadImportFileByPathDto.path)); + }); + + it('should normalize path before importing and not search for file outside home folder', async () => { + mockedFs.pathExists.mockImplementationOnce(async () => true); + + await service.uploadFromTutorial(mockClientMetadata, { + path: '/../../../danger', + }); + + expect(mockedFs.readFile).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, 'danger')); + }); + + it('should normalize path before importing and not search for file outside home folder (relative)', async () => { + mockedFs.pathExists.mockImplementationOnce(async () => true); + + await service.uploadFromTutorial(mockClientMetadata, { + path: '../../../danger', + }); + + expect(mockedFs.readFile).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, 'danger')); + }); + + it('should throw BadRequest when no file found', async () => { + mockedFs.pathExists.mockImplementationOnce(async () => false); + + try { + await service.uploadFromTutorial(mockClientMetadata, { + path: '../../../danger', + }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + expect(e.message).toEqual('Data file was not found'); + } + }); + }); }); diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts index 13919df4ec..28bfd70373 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts @@ -1,4 +1,6 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { join, resolve } from 'path'; +import * as fs from 'fs-extra'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { Readable } from 'stream'; import * as readline from 'readline'; import { wrapHttpError } from 'src/common/utils'; @@ -10,8 +12,12 @@ import { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-s import { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface'; import { BulkActionStatus, BulkActionType } from 'src/modules/bulk-actions/constants'; import { BulkActionsAnalyticsService } from 'src/modules/bulk-actions/bulk-actions-analytics.service'; +import { UploadImportFileByPathDto } from 'src/modules/bulk-actions/dto/upload-import-file-by-path.dto'; +import config from 'src/utils/config'; +import { MemoryStoredFile } from 'nestjs-form-data'; const BATCH_LIMIT = 10_000; +const PATH_CONFIG = config.get('dir_path'); @Injectable() export class BulkImportService { @@ -131,4 +137,31 @@ export class BulkImportService { throw wrapHttpError(e); } } + + /** + * Upload file from tutorial by path + * @param clientMetadata + * @param dto + */ + public async uploadFromTutorial( + clientMetadata: ClientMetadata, + dto: UploadImportFileByPathDto, + ): Promise { + try { + const path = join(PATH_CONFIG.homedir, resolve('/', dto.path)); + + if (!await fs.pathExists(path)) { + throw new BadRequestException('Data file was not found'); + } + + const buffer = await fs.readFile(path); + + return this.import(clientMetadata, { + file: { buffer } as MemoryStoredFile, + }); + } catch (e) { + this.logger.error('Unable to process an import file path from tutorial', e); + throw wrapHttpError(e); + } + } } diff --git a/redisinsight/api/src/modules/bulk-actions/dto/upload-import-file-by-path.dto.ts b/redisinsight/api/src/modules/bulk-actions/dto/upload-import-file-by-path.dto.ts new file mode 100644 index 0000000000..d9a73964fd --- /dev/null +++ b/redisinsight/api/src/modules/bulk-actions/dto/upload-import-file-by-path.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UploadImportFileByPathDto { + @ApiProperty({ + type: 'string', + description: 'Internal path to data file', + }) + @IsString() + @IsNotEmpty() + path: string; +} diff --git a/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts b/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts new file mode 100644 index 0000000000..49ed7a7ef9 --- /dev/null +++ b/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts @@ -0,0 +1,86 @@ +import { + expect, + describe, + it, + deps, + requirements, + validateApiCall, fsExtra, _ +} from '../deps'; +import { AdmZip, path } from '../../helpers/test'; +const { rte, request, server, constants } = deps; + +const endpoint = ( + id = constants.TEST_INSTANCE_ID, +) => request(server).post(`/${constants.API.DATABASES}/${id}/bulk-actions/import/tutorial-data`); + +const creatCustomTutorialsEndpoint = () => request(server).post(`/custom-tutorials`); + +const getZipArchive = () => { + const zipArchive = new AdmZip(); + + zipArchive.addFile('info.md', Buffer.from('# info.md', 'utf8')); + zipArchive.addFile('_data/data.txt', Buffer.from( + `set ${constants.TEST_STRING_KEY_1} bulkimport`, + 'utf8', + )); + + return zipArchive; +} + +describe('POST /databases/:id/bulk-actions/import/tutorial-data', () => { + requirements('!rte.sharedData', '!rte.bigData') + beforeEach(async () => await rte.data.truncate()); + + describe('Common', function () { + let tutorialId; + it('should import data', async () => { + expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.not.eq('bulkimport'); + + // create tutorial + const zip = getZipArchive(); + await validateApiCall({ + endpoint: creatCustomTutorialsEndpoint, + attach: ['file', zip.toBuffer(), 'a.zip'], + statusCode: 201, + checkFn: ({ body }) => { + tutorialId = body.id; + }, + }); + + await validateApiCall({ + endpoint, + data: { + path: path.join('/custom-tutorials', tutorialId, '_data/data.txt'), + }, + responseBody: { + id: 'empty', + databaseId: constants.TEST_INSTANCE_ID, + type: 'import', + summary: { processed: 1, succeed: 1, failed: 0, errors: [] }, + progress: null, + filter: null, + status: 'completed', + }, + checkFn: async ({ body }) => { + expect(body.duration).to.gt(0); + + expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eq('bulkimport'); + }, + }); + }); + it('should return BadRequest when path does not exists', async () => { + await validateApiCall({ + endpoint, + data: { + path: path.join('/custom-tutorials', tutorialId, '../../../../../_data/data.txt'), + }, + statusCode: 400, + responseBody: { + statusCode: 400, + message: 'Data file was not found', + error: 'Bad Request', + }, + }); + }); + }); +}); diff --git a/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-upload.test.ts b/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import.test.ts similarity index 100% rename from redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-upload.test.ts rename to redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import.test.ts From 6bb75fbe68874fe7788924043571cf4407e018c0 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 19 Apr 2023 09:15:10 +0300 Subject: [PATCH 02/32] #RI-4403 add support for url base and static folders --- redisinsight/api/config/default.ts | 1 + .../bulk-actions/bulk-import.service.spec.ts | 8 +++++ .../bulk-actions/bulk-import.service.ts | 15 +++++++- ...-bulk_actions-import-tutorial_data.test.ts | 35 +++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/redisinsight/api/config/default.ts b/redisinsight/api/config/default.ts index c163451dfe..013f9adb08 100644 --- a/redisinsight/api/config/default.ts +++ b/redisinsight/api/config/default.ts @@ -50,6 +50,7 @@ export default { contentUri: '/static/content', defaultPluginsUri: '/static/plugins', pluginsAssetsUri: '/static/resources/plugins', + base: process.env.RI_BASE || '/', secretStoragePassword: process.env.SECRET_STORAGE_PASSWORD, tls: process.env.SERVER_TLS ? process.env.SERVER_TLS === 'true' : true, tlsCert: process.env.SERVER_TLS_CERT, diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts index bffd669c7e..9ae66ff618 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts @@ -217,6 +217,14 @@ describe('BulkImportService', () => { expect(mockedFs.readFile).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, mockUploadImportFileByPathDto.path)); }); + it('should import file by path with static', async () => { + mockedFs.pathExists.mockImplementationOnce(async () => true); + + await service.uploadFromTutorial(mockClientMetadata, { path: '/static/guides/_data.file' }); + + expect(mockedFs.readFile).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, '/guides/_data.file')); + }); + it('should normalize path before importing and not search for file outside home folder', async () => { mockedFs.pathExists.mockImplementationOnce(async () => true); diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts index 28bfd70373..987ffac126 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts @@ -18,6 +18,7 @@ import { MemoryStoredFile } from 'nestjs-form-data'; const BATCH_LIMIT = 10_000; const PATH_CONFIG = config.get('dir_path'); +const SERVER_CONFIG = config.get('server'); @Injectable() export class BulkImportService { @@ -148,7 +149,19 @@ export class BulkImportService { dto: UploadImportFileByPathDto, ): Promise { try { - const path = join(PATH_CONFIG.homedir, resolve('/', dto.path)); + const staticPath = join(SERVER_CONFIG.base, SERVER_CONFIG.staticUri); + + let trimmedPath = dto.path; + if (dto.path.indexOf(staticPath) === 0) { + trimmedPath = dto.path.slice(staticPath.length); + } + + const resolvedPath = resolve( + '/', + trimmedPath, + ); + + const path = join(PATH_CONFIG.homedir, resolvedPath); if (!await fs.pathExists(path)) { throw new BadRequestException('Data file was not found'); diff --git a/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts b/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts index 49ed7a7ef9..4756b45550 100644 --- a/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts +++ b/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts @@ -68,6 +68,41 @@ describe('POST /databases/:id/bulk-actions/import/tutorial-data', () => { }, }); }); + it('should import data with static path', async () => { + expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.not.eq('bulkimport'); + + // create tutorial + const zip = getZipArchive(); + await validateApiCall({ + endpoint: creatCustomTutorialsEndpoint, + attach: ['file', zip.toBuffer(), 'a.zip'], + statusCode: 201, + checkFn: ({ body }) => { + tutorialId = body.id; + }, + }); + + await validateApiCall({ + endpoint, + data: { + path: path.join('/static/custom-tutorials', tutorialId, '_data/data.txt'), + }, + responseBody: { + id: 'empty', + databaseId: constants.TEST_INSTANCE_ID, + type: 'import', + summary: { processed: 1, succeed: 1, failed: 0, errors: [] }, + progress: null, + filter: null, + status: 'completed', + }, + checkFn: async ({ body }) => { + expect(body.duration).to.gt(0); + + expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eq('bulkimport'); + }, + }); + }); it('should return BadRequest when path does not exists', async () => { await validateApiCall({ endpoint, From c59b99231e8466a8ed21dce0c4f05ca7fe032676 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 19 Apr 2023 09:57:13 +0300 Subject: [PATCH 03/32] #RI-4404 core path generators + tests + initial ui implementation --- redisinsight/ui/src/constants/api.ts | 1 + .../components/InternalPage/InternalPage.tsx | 3 +- .../RedisUploadButton/RedisUploadButton.tsx | 57 +++++++++++++++++++ .../components/RedisUploadButton/index.ts | 3 + .../EnablementArea/components/index.ts | 2 + .../utils/formatter/MarkdownToJsxString.ts | 2 + .../utils/tests/remarkImage.spec.ts | 4 +- .../utils/tests/remarkRedisUpload.spec.ts | 53 +++++++++++++++++ .../utils/transform/remarkImage.ts | 6 +- .../utils/transform/remarkRedisUpload.ts | 23 ++++++++ redisinsight/ui/src/utils/pathUtil.ts | 39 +++++++++++++ .../ui/src/utils/tests/pathUtil.spec.ts | 41 +++++++++++++ 12 files changed, 227 insertions(+), 7 deletions(-) create mode 100644 redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx create mode 100644 redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/index.ts create mode 100644 redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkRedisUpload.spec.ts create mode 100644 redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts create mode 100644 redisinsight/ui/src/utils/pathUtil.ts create mode 100644 redisinsight/ui/src/utils/tests/pathUtil.spec.ts diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index 916bacdfb9..20ca803f3c 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -5,6 +5,7 @@ enum ApiEndpoints { DATABASES_EXPORT = 'databases/export', BULK_ACTIONS_IMPORT = 'bulk-actions/import', + BULK_ACTIONS_IMPORT_TUTORIAL_DATA = 'bulk-actions/import/tutorial-data', CA_CERTIFICATES = 'certificates/ca', CLIENT_CERTIFICATES = 'certificates/client', diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/InternalPage.tsx b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/InternalPage.tsx index 471b023561..6ea62df990 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/InternalPage.tsx +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/InternalPage.tsx @@ -16,6 +16,7 @@ import { Image, Code, EmptyPrompt, + RedisUploadButton, Pagination } from 'uiSrc/pages/workbench/components/enablement-area/EnablementArea/components' import { getTutorialSection } from 'uiSrc/pages/workbench/components/enablement-area/EnablementArea/utils' @@ -58,7 +59,7 @@ const InternalPage = (props: Props) => { manifestPath, sourcePath } = props - const components: any = { LazyCodeButton, Image, Code } + const components: any = { LazyCodeButton, Image, Code, RedisUploadButton } const containerRef = useRef(null) const { instanceId = '' } = useParams<{ instanceId: string }>() const handleScroll = debounce(() => { diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx new file mode 100644 index 0000000000..152d4021b8 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx @@ -0,0 +1,57 @@ +import { EuiButton } from '@elastic/eui' +import { useSelector } from 'react-redux' +import { AxiosError } from 'axios' +import React, { useState } from 'react' +import { getApiErrorMessage, getUrl, isStatusSuccessful, truncateText } from 'uiSrc/utils' +import { ApiEndpoints } from 'uiSrc/constants' +import { apiService } from 'uiSrc/services' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' + +export interface Props { + label: string + path: string +} + +const RedisUploadButton = ({ label, path }: Props) => { + const [isLoading, setIsLoading] = useState(false) + const { id } = useSelector(connectedInstanceSelector) + + const uploadData = async () => { + setIsLoading(true) + + try { + const { status, data } = await apiService.post( + getUrl( + id, + ApiEndpoints.BULK_ACTIONS_IMPORT_TUTORIAL_DATA + ), + { path }, + ) + + if (isStatusSuccessful(status)) { + // todo: result data message + } + } catch (error) { + const errorMessage = getApiErrorMessage(error as AxiosError) + // todo: show error message + } + + setIsLoading(false) + } + + return ( + + {truncateText(label, 86)} + + ) +} + +export default RedisUploadButton diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/index.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/index.ts new file mode 100644 index 0000000000..fc79773633 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/index.ts @@ -0,0 +1,3 @@ +import RedisUploadButton from './RedisUploadButton' + +export default RedisUploadButton diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/index.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/index.ts index 5f7f693945..52c5608c3e 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/index.ts +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/index.ts @@ -11,6 +11,7 @@ import PlainText from './PlainText' import Pagination from './Pagination' import UploadTutorialForm from './UploadTutorialForm' import WelcomeMyTutorials from './WelcomeMyTutorials' +import RedisUploadButton from './RedisUploadButton' export { Code, @@ -26,4 +27,5 @@ export { Pagination, UploadTutorialForm, WelcomeMyTutorials, + RedisUploadButton, } diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/formatter/MarkdownToJsxString.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/formatter/MarkdownToJsxString.ts index a2b0e2c393..889b48b444 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/formatter/MarkdownToJsxString.ts +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/formatter/MarkdownToJsxString.ts @@ -7,6 +7,7 @@ import { visit } from 'unist-util-visit' import { IFormatter, IFormatterConfig } from './formatter.interfaces' import { rehypeLinks } from '../transform/rehypeLinks' +import { remarkRedisUpload } from '../transform/remarkRedisUpload' import { remarkRedisCode } from '../transform/remarkRedisCode' import { remarkImage } from '../transform/remarkImage' @@ -17,6 +18,7 @@ class MarkdownToJsxString implements IFormatter { unified() .use(remarkParse) .use(remarkGfm) // support GitHub Flavored Markdown + .use(remarkRedisUpload, path) // Add custom component for redis-upload code block .use(remarkRedisCode) // Add custom component for Redis code block .use(remarkImage, path) // Add custom component for Redis code block .use(remarkRehype, { allowDangerousHtml: true }) // Pass raw HTML strings through. diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkImage.spec.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkImage.spec.ts index 50474b6c75..0e2747d380 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkImage.spec.ts +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkImage.spec.ts @@ -11,9 +11,9 @@ const testCases = [ result: `${RESOURCES_BASE_URL}${TUTORIAL_PATH}/_images/relative.png`, }, { - url: '/../../../_images/relative.png', // NOTE: will not work in real. There is no sense to even support absolute paths + url: '/_images/relative.png', path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`, - result: `${RESOURCES_BASE_URL}_images/relative.png`, + result: `${RESOURCES_BASE_URL}${TUTORIAL_PATH}/_images/relative.png`, }, { url: 'https://somesite.test/image.png', diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkRedisUpload.spec.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkRedisUpload.spec.ts new file mode 100644 index 0000000000..f445f9f63c --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkRedisUpload.spec.ts @@ -0,0 +1,53 @@ +import { visit } from 'unist-util-visit' +import { remarkRedisUpload } from '../transform/remarkRedisUpload' + +jest.mock('unist-util-visit') + +const getValue = (label: string, path: string) => + `` + +const TUTORIAL_PATH = 'static/custom-tutorials/tutorial-id' + +const testCases = [ + { + value: 'redis-upload:[../../../_data/strings.txt] Upload data', + path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`, + label: 'Upload data', + resultPath: `/${TUTORIAL_PATH}/_data/strings.txt` + }, + { + value: 'redis-upload:[/_data/strings.txt] Upload data', + path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`, + label: 'Upload data', + resultPath: `/${TUTORIAL_PATH}/_data/strings.txt` + }, + { + value: 'redis-upload:[https://somesite.test/image.png] Upload data', + path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`, + label: 'Upload data', + resultPath: '/image.png', + }, +] + +describe('remarkRedisUpload', () => { + testCases.forEach((tc) => { + it(`should return ${tc.resultPath} + ${tc.label} for url:${tc.value}, path: ${tc.path} `, () => { + const node = { + type: 'inlineCode', + value: tc.value, + }; + + // mock implementation + (visit as jest.Mock) + .mockImplementation((_tree: any, _name: string, callback: (node: any) => void) => { callback(node) }) + + const remark = remarkRedisUpload(tc.path) + remark({} as Node) + expect(node).toEqual({ + ...node, + type: 'html', + value: getValue(tc.label, tc.resultPath), + }) + }) + }) +}) diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkImage.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkImage.ts index ba5aacfdda..1943ebbd3a 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkImage.ts +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkImage.ts @@ -1,11 +1,9 @@ import { visit } from 'unist-util-visit' -import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService' +import { prepareTutorialDataFileUrlFromMd } from 'uiSrc/utils/pathUtil' export const remarkImage = (path: string): (tree: Node) => void => (tree: any) => { // Find img node in syntax tree visit(tree, 'image', (node) => { - const pathURL = new URL(path, RESOURCES_BASE_URL) - const url = new URL(node.url, pathURL) - node.url = url.toString() + node.url = prepareTutorialDataFileUrlFromMd(node.url, path) }) } diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts new file mode 100644 index 0000000000..448f5fb36d --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts @@ -0,0 +1,23 @@ +import { visit } from 'unist-util-visit' +import { prepareTutorialDataFileUrlFromMd } from 'uiSrc/utils/pathUtil' + +export const remarkRedisUpload = (path: string): (tree: Node) => void => (tree: any) => { + // Find code node in syntax tree + visit(tree, 'inlineCode', (node) => { + try { + const { value } = node + + const [, filePath, label] = value.match(/^redis-upload:\[(.*)] (.*)/i) + + const { pathname } = new URL(prepareTutorialDataFileUrlFromMd(filePath, path)) + + if (path && label) { + node.type = 'html' + // Replace it with our custom component + node.value = `` + } + } catch (e) { + // ignore errors + } + }) +} diff --git a/redisinsight/ui/src/utils/pathUtil.ts b/redisinsight/ui/src/utils/pathUtil.ts new file mode 100644 index 0000000000..5528d8ac96 --- /dev/null +++ b/redisinsight/ui/src/utils/pathUtil.ts @@ -0,0 +1,39 @@ +import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService' +import { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex' + +enum TutorialsPaths { + CustomTutorials = 'custom-tutorials', + Guide = 'guides', + Tutorials = 'tutorials', +} + +export const prepareTutorialDataFileUrlFromMd = (nodeUrl: string, mdPath: string): string => { + // process external link + if (IS_ABSOLUTE_PATH.test(nodeUrl)) { + return nodeUrl + } + + // process absolute path + if (nodeUrl.startsWith('/') || nodeUrl.startsWith('\\')) { + const paths = mdPath?.split('/') || [] + let tutorialRootPath + switch (paths[1]) { + case TutorialsPaths.CustomTutorials: + tutorialRootPath = paths.slice(0, 3).join('/') + break + case TutorialsPaths.Guide: + case TutorialsPaths.Tutorials: + tutorialRootPath = paths.slice(0, 2).join('/') + break + default: + tutorialRootPath = mdPath + break + } + + return new URL(tutorialRootPath + nodeUrl, RESOURCES_BASE_URL).toString() + } + + // process relative path + const pathUrl = new URL(mdPath, RESOURCES_BASE_URL) + return new URL(nodeUrl, pathUrl).toString() +} diff --git a/redisinsight/ui/src/utils/tests/pathUtil.spec.ts b/redisinsight/ui/src/utils/tests/pathUtil.spec.ts new file mode 100644 index 0000000000..1177029eec --- /dev/null +++ b/redisinsight/ui/src/utils/tests/pathUtil.spec.ts @@ -0,0 +1,41 @@ +import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService' +import { prepareTutorialDataFileUrlFromMd } from '../pathUtil' + +jest.mock('unist-util-visit') +const TUTORIAL_PATH = 'static/custom-tutorials/tutorial-id' +const GUIDES_PATH = 'static/guides' +const testCases = [ + { + url: '../../../_images/relative.png', + path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`, + result: `${RESOURCES_BASE_URL}${TUTORIAL_PATH}/_images/relative.png`, + }, + { + url: '/_images/relative.png', + path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`, + result: `${RESOURCES_BASE_URL}${TUTORIAL_PATH}/_images/relative.png`, + }, + { + url: '/_images/relative.png', + path: `${GUIDES_PATH}/lvl1/lvl2/lvl3/intro.md`, + result: `${RESOURCES_BASE_URL}${GUIDES_PATH}/_images/relative.png`, + }, + { + url: '/_images/relative.png', + path: '/unknown-path/lvl1/lvl2/lvl3/intro.md', + result: `${RESOURCES_BASE_URL}/unknown-path/lvl1/lvl2/lvl3/intro.md/_images/relative.png`, + }, + { + url: 'https://somesite.test/image.png', + path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`, + result: 'https://somesite.test/image.png', + } +] +describe('prepareTutorialDataFileUrlFromMd', () => { + testCases.forEach((tc) => { + it(`should return ${tc.result} for url:${tc.url}, path: ${tc.path} `, () => { + const url = prepareTutorialDataFileUrlFromMd(tc.url, tc.path) + expect(url).toEqual(tc.result) + }) + }) +}) From e69909b8089e8de94ddc8ad33dbbdb28d8e0b6a1 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 19 Apr 2023 11:12:32 +0300 Subject: [PATCH 04/32] #RI-4404 fix tests --- ...POST-databases-id-bulk_actions-import-tutorial_data.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts b/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts index 4756b45550..37b2a74cdb 100644 --- a/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts +++ b/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts @@ -28,7 +28,8 @@ const getZipArchive = () => { } describe('POST /databases/:id/bulk-actions/import/tutorial-data', () => { - requirements('!rte.sharedData', '!rte.bigData') + requirements('!rte.sharedData', '!rte.bigData', 'rte.serverType=local') + beforeEach(async () => await rte.data.truncate()); describe('Common', function () { From b8270131cf107c8c6c8a1126c809bacf8b48cc6e Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 19 Apr 2023 12:16:25 +0300 Subject: [PATCH 05/32] fix tests --- ...bases-id-bulk_actions-import-tutorial_data.test.ts | 2 +- .../custom-tutorials/POST-custom-tutorials.test.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts b/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts index 37b2a74cdb..0c4534dce3 100644 --- a/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts +++ b/redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts @@ -4,7 +4,7 @@ import { it, deps, requirements, - validateApiCall, fsExtra, _ + validateApiCall, } from '../deps'; import { AdmZip, path } from '../../helpers/test'; const { rte, request, server, constants } = deps; diff --git a/redisinsight/api/test/api/custom-tutorials/POST-custom-tutorials.test.ts b/redisinsight/api/test/api/custom-tutorials/POST-custom-tutorials.test.ts index 983bd5499a..6fec8089e7 100644 --- a/redisinsight/api/test/api/custom-tutorials/POST-custom-tutorials.test.ts +++ b/redisinsight/api/test/api/custom-tutorials/POST-custom-tutorials.test.ts @@ -12,7 +12,7 @@ import { _, } from '../deps'; import { getBaseURL } from '../../helpers/server'; -const { server, request } = deps; +const { server, request, localDb } = deps; // create endpoint const creatEndpoint = () => request(server).post(`/custom-tutorials`); @@ -110,11 +110,12 @@ const globalManifest = { describe('POST /custom-tutorials', () => { requirements('rte.serverType=local'); - describe('Common', () => { - before(async () => { - await fsExtra.remove(customTutorialsFolder); - }); + before(async () => { + await fsExtra.remove(customTutorialsFolder); + await (await localDb.getRepository(localDb.repositories.CUSTOM_TUTORIAL)).clear(); + }); + describe('Common', () => { it('should import tutorial from file and generate _manifest.json', async () => { const zip = getZipArchive(); zip.writeZip(path.join(staticsFolder, 'test_no_manifest.zip')); From 7f4225bac25d3c2e6bd9b68135d435351e044fe0 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 19 Apr 2023 19:32:04 +0300 Subject: [PATCH 06/32] add file size validation --- .../bulk-actions/bulk-import.service.spec.ts | 15 +++++++++++++++ .../modules/bulk-actions/bulk-import.service.ts | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts index 9ae66ff618..5da086fedf 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts @@ -258,5 +258,20 @@ describe('BulkImportService', () => { expect(e.message).toEqual('Data file was not found'); } }); + + it('should throw BadRequest when file size is greater then 100MB', async () => { + mockedFs.pathExists.mockImplementationOnce(async () => true); + mockedFs.stat.mockImplementationOnce(async () => ({ size: 100 * 1024 * 1024 + 1 } as fs.Stats)); + + try { + await service.uploadFromTutorial(mockClientMetadata, { + path: '../../../danger', + }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + expect(e.message).toEqual('Maximum file size is 100MB'); + } + }); }); }); diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts index 411956aedb..98255228e0 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts @@ -168,6 +168,10 @@ export class BulkImportService { throw new BadRequestException('Data file was not found'); } + if ((await fs.stat(path))?.size > 100 * 1024 * 1024) { + throw new BadRequestException('Maximum file size is 100MB'); + } + const buffer = await fs.readFile(path); return this.import(clientMetadata, { From 85ffa73ee1633963a540abd0ba03422a69e2e87a Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 26 Apr 2023 14:42:55 +0200 Subject: [PATCH 07/32] #RI-4404 - finish upload data in bulk for tutorials --- .../src/assets/img/icons/data-upload-bulk.svg | 3 + .../notifications/Notifications.tsx | 3 +- .../notifications/styles.module.scss | 13 ++ .../notifications/success-messages.tsx | 47 +++++- .../RedisUploadButton.spec.tsx | 95 +++++++++++ .../RedisUploadButton/RedisUploadButton.tsx | 128 ++++++++++----- .../RedisUploadButton/styles.module.scss | 63 ++++++++ .../utils/tests/remarkRedisUpload.spec.ts | 23 +-- .../utils/transform/remarkRedisUpload.ts | 10 +- redisinsight/ui/src/slices/interfaces/app.ts | 1 + .../ui/src/slices/interfaces/workbench.ts | 5 + .../workbench/wb-custom-tutorials.spec.ts | 153 +++++++++++++++++- .../slices/workbench/wb-custom-tutorials.ts | 73 +++++++-- .../ui/src/styles/components/_toasts.scss | 14 ++ redisinsight/ui/src/telemetry/events.ts | 2 + redisinsight/ui/src/utils/pathUtil.ts | 2 + 16 files changed, 566 insertions(+), 69 deletions(-) create mode 100644 redisinsight/ui/src/assets/img/icons/data-upload-bulk.svg create mode 100644 redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.spec.tsx create mode 100644 redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/styles.module.scss diff --git a/redisinsight/ui/src/assets/img/icons/data-upload-bulk.svg b/redisinsight/ui/src/assets/img/icons/data-upload-bulk.svg new file mode 100644 index 0000000000..129402a0bf --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/data-upload-bulk.svg @@ -0,0 +1,3 @@ + + + diff --git a/redisinsight/ui/src/components/notifications/Notifications.tsx b/redisinsight/ui/src/components/notifications/Notifications.tsx index e544637d19..c9cb8a2df8 100644 --- a/redisinsight/ui/src/components/notifications/Notifications.tsx +++ b/redisinsight/ui/src/components/notifications/Notifications.tsx @@ -64,7 +64,7 @@ const Notifications = () => { ) const getSuccessToasts = (data: IMessage[]) => - data.map(({ id = '', title = '', message = '', group }) => { + data.map(({ id = '', title = '', message = '', className, group }) => { const toast: Toast = { id, iconType: 'iInCircle', @@ -74,6 +74,7 @@ const Notifications = () => { ), color: 'success', + className } toast.text = getSuccessText(message, toast, group) toast.onClose = () => removeToast(toast) diff --git a/redisinsight/ui/src/components/notifications/styles.module.scss b/redisinsight/ui/src/components/notifications/styles.module.scss index c4432a06cd..96f7f3fb34 100644 --- a/redisinsight/ui/src/components/notifications/styles.module.scss +++ b/redisinsight/ui/src/components/notifications/styles.module.scss @@ -16,3 +16,16 @@ :global(.euiToast) { box-shadow: none !important; } + +.summary { + border-top: 1px solid var(--euiToastSuccessBtnColor); + padding-top: 8px; + + :global(.euiFlexItem:not(:last-child)) { + margin-right: 14px !important; + } + .summaryValue { + font-size: 14px; + font-weight: 500; + } +} diff --git a/redisinsight/ui/src/components/notifications/success-messages.tsx b/redisinsight/ui/src/components/notifications/success-messages.tsx index e5d2608e26..9260e36f8c 100644 --- a/redisinsight/ui/src/components/notifications/success-messages.tsx +++ b/redisinsight/ui/src/components/notifications/success-messages.tsx @@ -1,7 +1,9 @@ import React from 'react' +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' -import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { bufferToString, formatNameShort, Maybe } from 'uiSrc/utils' +import { IBulkActionOverview, RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { bufferToString, formatLongName, formatNameShort, Maybe, millisecondsFormat } from 'uiSrc/utils' +import { numberWithSpaces } from 'uiSrc/utils/numbers' import styles from './styles.module.scss' // TODO: use i18n file for texts @@ -153,5 +155,44 @@ export default { }), TEST_CONNECTION: () => ({ title: 'Connection is successful', - }) + }), + UPLOAD_DATA_BULK: (data: IBulkActionOverview, fileName: string) => { + const { processed = 0, succeed = 0, failed = 0, } = data?.summary ?? {} + return ({ + title: ( + <> + Action completed +
+ Data uploaded with file: {formatLongName(fileName, 24, 5)} + + ), + message: ( + + + {numberWithSpaces(processed)} + Commands Processed + + + {numberWithSpaces(succeed)} + Success + + + {numberWithSpaces(failed)} + Errors + + + {millisecondsFormat(data?.duration || 0, 'H:mm:ss.SSS')} + Time Taken + + + ), + className: 'dynamic' + }) + } } diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.spec.tsx b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.spec.tsx new file mode 100644 index 0000000000..4de5dc3c31 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.spec.tsx @@ -0,0 +1,95 @@ +import React from 'react' +import { cloneDeep } from 'lodash' +import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' +import { customTutorialsBulkUploadSelector, uploadDataBulk } from 'uiSrc/slices/workbench/wb-custom-tutorials' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import RedisUploadButton, { Props } from './RedisUploadButton' + +jest.mock('uiSrc/slices/instances/instances', () => ({ + ...jest.requireActual('uiSrc/slices/instances/instances'), + connectedInstanceSelector: jest.fn().mockReturnValue({ + id: 'databaseId', + }), +})) + +jest.mock('uiSrc/slices/workbench/wb-custom-tutorials', () => ({ + ...jest.requireActual('uiSrc/slices/workbench/wb-custom-tutorials'), + customTutorialsBulkUploadSelector: jest.fn().mockReturnValue({ + pathsInProgress: [], + }), +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +const props: Props = { + label: 'Label', + path: '/text' +} + +describe('RedisUploadButton', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should be disabled with loading state', () => { + (customTutorialsBulkUploadSelector as jest.Mock).mockReturnValueOnce({ + pathsInProgress: [props.path], + }) + + render() + + expect(screen.getByTestId('upload-data-bulk-btn')).toBeDisabled() + }) + + it('should open warning popover and call proper actions after submit', () => { + render() + + fireEvent.click(screen.getByTestId('upload-data-bulk-btn')) + + expect(screen.getByTestId('upload-data-bulk-tooltip')).toBeInTheDocument() + + fireEvent.click(screen.getByTestId('upload-data-bulk-apply-btn')) + + const expectedActions = [uploadDataBulk(props.path)] + expect(store.getActions()).toEqual(expectedActions) + }) + + it('should call proper telemetry events', () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + render() + + fireEvent.click(screen.getByTestId('upload-data-bulk-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_CLICKED, + eventData: { + databaseId: 'databaseId' + } + }); + + (sendEventTelemetry as jest.Mock).mockRestore() + + fireEvent.click(screen.getByTestId('upload-data-bulk-apply-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_SUBMITTED, + eventData: { + databaseId: 'databaseId' + } + }); + + (sendEventTelemetry as jest.Mock).mockRestore() + }) +}) diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx index 152d4021b8..57588d8aee 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx @@ -1,11 +1,15 @@ -import { EuiButton } from '@elastic/eui' -import { useSelector } from 'react-redux' -import { AxiosError } from 'axios' -import React, { useState } from 'react' -import { getApiErrorMessage, getUrl, isStatusSuccessful, truncateText } from 'uiSrc/utils' -import { ApiEndpoints } from 'uiSrc/constants' -import { apiService } from 'uiSrc/services' +import { EuiButton, EuiIcon, EuiPopover, EuiSpacer, EuiText } from '@elastic/eui' +import { useDispatch, useSelector } from 'react-redux' +import React, { useEffect, useState } from 'react' +import cx from 'classnames' +import { truncateText } from 'uiSrc/utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { customTutorialsBulkUploadSelector, uploadDataBulkAction } from 'uiSrc/slices/workbench/wb-custom-tutorials' + +import { ReactComponent as BulkDataUploadIcon } from 'uiSrc/assets/img/icons/data-upload-bulk.svg' + +import styles from './styles.module.scss' export interface Props { label: string @@ -13,44 +17,90 @@ export interface Props { } const RedisUploadButton = ({ label, path }: Props) => { - const [isLoading, setIsLoading] = useState(false) - const { id } = useSelector(connectedInstanceSelector) + const { id: instanceId } = useSelector(connectedInstanceSelector) + const { pathsInProgress } = useSelector(customTutorialsBulkUploadSelector) - const uploadData = async () => { - setIsLoading(true) - - try { - const { status, data } = await apiService.post( - getUrl( - id, - ApiEndpoints.BULK_ACTIONS_IMPORT_TUTORIAL_DATA - ), - { path }, - ) - - if (isStatusSuccessful(status)) { - // todo: result data message + const [isLoading, setIsLoading] = useState(false) + const [isPopoverOpen, setIsPopoverOpen] = useState(false) + + const dispatch = useDispatch() + + useEffect(() => { + setIsLoading(pathsInProgress.includes(path)) + }, [pathsInProgress]) + + const openPopover = () => { + setIsPopoverOpen(true) + sendEventTelemetry({ + event: TelemetryEvent.WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_CLICKED, + eventData: { + databaseId: instanceId } - } catch (error) { - const errorMessage = getApiErrorMessage(error as AxiosError) - // todo: show error message - } + }) + } - setIsLoading(false) + const uploadData = async () => { + setIsPopoverOpen(false) + dispatch(uploadDataBulkAction(instanceId, path)) + sendEventTelemetry({ + event: TelemetryEvent.WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_SUBMITTED, + eventData: { + databaseId: instanceId + } + }) } return ( - - {truncateText(label, 86)} - +
+ setIsPopoverOpen(false)} + panelClassName={styles.panelPopover} + anchorClassName={styles.popoverAnchor} + panelPaddingSize="none" + button={( + + {truncateText(label, 86)} + + )} + > + + +
+ Execute commands in bulk +
+ +
+ All commands from the file in your tutorial will be automatically executed against your database. + Avoid executing them in production databases. +
+ + Execute + +
+
+
) } diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/styles.module.scss new file mode 100644 index 0000000000..f2b51a5640 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/styles.module.scss @@ -0,0 +1,63 @@ +.wrapper { + width: 100%; + + :global(.euiPopover) { + width: 100%; + } + + .button { + &[class*='euiButton--secondary']:not([class*='isDisabled']) { + &:hover { + background-color: var(--euiColorSecondary) !important; + border-color: var(--euiColorSecondary) !important; + color: var(--euiColorPrimaryText) !important; + } + } + &:global(.euiButton.euiButton-isDisabled) { + color: var(--buttonSecondaryDisabledTextColor) !important; + } + } +} + +.containerPopover { + padding: 18px; + width: 360px; + height: 188px; +} + +.panelPopover { + border-color: var(--euiColorPrimary) !important; + :global(.euiPopover__panelArrow:before) { + border-bottom-color: var(--euiColorPrimary) !important; + } +} + +.popoverAnchor { + display: block; + width: 100%; +} + +.popoverIcon { + position: absolute; + color: var(--euiColorWarningLight) !important; + width: 24px !important; + height: 24px !important; +} + +.popoverItem { + font-size: 13px !important; + line-height: 18px !important; + padding-left: 34px; +} + +.popoverItemTitle { + color: var(--htmlColor) !important; + font-size: 14px !important; + line-height: 24px !important; +} + +.uploadApproveBtn { + position: absolute; + right: 18px; + bottom: 18px; +} diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkRedisUpload.spec.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkRedisUpload.spec.ts index f445f9f63c..ea5b358589 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkRedisUpload.spec.ts +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/remarkRedisUpload.spec.ts @@ -10,31 +10,32 @@ const TUTORIAL_PATH = 'static/custom-tutorials/tutorial-id' const testCases = [ { - value: 'redis-upload:[../../../_data/strings.txt] Upload data', + lang: 'redis-upload:[../../../_data/strings.txt]', path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`, - label: 'Upload data', + meta: 'Upload data', resultPath: `/${TUTORIAL_PATH}/_data/strings.txt` }, { - value: 'redis-upload:[/_data/strings.txt] Upload data', + lang: 'redis-upload:[/_data/s t rings.txt]', path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`, - label: 'Upload data', - resultPath: `/${TUTORIAL_PATH}/_data/strings.txt` + meta: 'Upload data', + resultPath: `/${TUTORIAL_PATH}/_data/s t rings.txt` }, { - value: 'redis-upload:[https://somesite.test/image.png] Upload data', + lang: 'redis-upload:[https://somesite.test/image.png]', path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`, - label: 'Upload data', + meta: 'Upload data', resultPath: '/image.png', }, ] describe('remarkRedisUpload', () => { testCases.forEach((tc) => { - it(`should return ${tc.resultPath} + ${tc.label} for url:${tc.value}, path: ${tc.path} `, () => { + it(`should return ${tc.resultPath} + ${tc.meta} for ${tc.lang} ${tc.meta}`, () => { const node = { - type: 'inlineCode', - value: tc.value, + type: 'code', + lang: tc.lang, + meta: tc.meta }; // mock implementation @@ -46,7 +47,7 @@ describe('remarkRedisUpload', () => { expect(node).toEqual({ ...node, type: 'html', - value: getValue(tc.label, tc.resultPath), + value: getValue(tc.meta, tc.resultPath), }) }) }) diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts index 448f5fb36d..54ce480901 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts @@ -3,18 +3,20 @@ import { prepareTutorialDataFileUrlFromMd } from 'uiSrc/utils/pathUtil' export const remarkRedisUpload = (path: string): (tree: Node) => void => (tree: any) => { // Find code node in syntax tree - visit(tree, 'inlineCode', (node) => { + visit(tree, 'code', (node) => { try { - const { value } = node + const { lang, meta } = node - const [, filePath, label] = value.match(/^redis-upload:\[(.*)] (.*)/i) + const value: string = `${lang} ${meta}` + const [, filePath, label] = value.match(/^redis-upload:\[(.*)] (.*)/i) || [] const { pathname } = new URL(prepareTutorialDataFileUrlFromMd(filePath, path)) + const decodedPath = decodeURI(pathname) if (path && label) { node.type = 'html' // Replace it with our custom component - node.value = `` + node.value = `` } } catch (e) { // ignore errors diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index 397d5b9e62..9a690ef1c2 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -16,6 +16,7 @@ export interface IMessage { title: string message: string group?: string + className?: string } export interface StateAppInfo { diff --git a/redisinsight/ui/src/slices/interfaces/workbench.ts b/redisinsight/ui/src/slices/interfaces/workbench.ts index 94c7220d67..a304df6540 100644 --- a/redisinsight/ui/src/slices/interfaces/workbench.ts +++ b/redisinsight/ui/src/slices/interfaces/workbench.ts @@ -39,6 +39,11 @@ export interface StateWorkbenchEnablementArea { error: string items: IEnablementAreaItem[] } +export interface StateWorkbenchCustomTutorials extends StateWorkbenchEnablementArea { + bulkUpload: { + pathsInProgress: string[] + } +} export interface CommandExecutionUI extends Partial { id?: string diff --git a/redisinsight/ui/src/slices/tests/workbench/wb-custom-tutorials.spec.ts b/redisinsight/ui/src/slices/tests/workbench/wb-custom-tutorials.spec.ts index 13c66a2ba1..8ffeca8649 100644 --- a/redisinsight/ui/src/slices/tests/workbench/wb-custom-tutorials.spec.ts +++ b/redisinsight/ui/src/slices/tests/workbench/wb-custom-tutorials.spec.ts @@ -1,11 +1,13 @@ import { cloneDeep } from 'lodash' import { AxiosError } from 'axios' import { cleanup, initialStateDefault, mockedStore, } from 'uiSrc/utils/test-utils' -import { IEnablementAreaItem } from 'uiSrc/slices/interfaces' +import { IBulkActionOverview, IEnablementAreaItem } from 'uiSrc/slices/interfaces' import { MOCK_CUSTOM_TUTORIALS, MOCK_TUTORIALS_ITEMS } from 'uiSrc/constants' import { apiService } from 'uiSrc/services' -import { addErrorNotification } from 'uiSrc/slices/app/notifications' +import { addErrorNotification, addMessageNotification } from 'uiSrc/slices/app/notifications' +import successMessages from 'uiSrc/components/notifications/success-messages' +import { getFileNameFromPath } from 'uiSrc/utils/pathUtil' import reducer, { initialState, getWBCustomTutorials, @@ -21,6 +23,10 @@ import reducer, { fetchCustomTutorials, deleteCustomTutorial, workbenchCustomTutorialsSelector, + uploadDataBulk, + uploadDataBulkSuccess, + uploadDataBulkFailed, + uploadDataBulkAction, defaultItems, } from '../../workbench/wb-custom-tutorials' @@ -276,6 +282,97 @@ describe('slices', () => { }) }) + describe('uploadDataBulk', () => { + it('should properly set loading for paths', () => { + // Arrange + const state = { + ...initialState, + bulkUpload: { + ...initialState.bulkUpload, + pathsInProgress: ['data/data'] + } + } + + // Act + const nextState = reducer(initialState, uploadDataBulk('data/data')) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + customTutorials: nextState, + }, + }) + + expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state) + }) + }) + + describe('uploadDataBulk', () => { + it('should properly remove path from loading', () => { + // Arrange + const currentState = { + ...initialState, + bulkUpload: { + ...initialState.bulkUpload, + pathsInProgress: ['data/data', 'data/another'] + } + } + + const state = { + ...initialState, + bulkUpload: { + ...initialState.bulkUpload, + pathsInProgress: ['data/another'] + } + } + + // Act + const nextState = reducer(currentState, uploadDataBulkSuccess('data/data')) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + customTutorials: nextState, + }, + }) + + expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state) + }) + }) + + describe('uploadDataBulkFailed', () => { + it('should properly remove path from loading', () => { + // Arrange + const currentState = { + ...initialState, + bulkUpload: { + ...initialState.bulkUpload, + pathsInProgress: ['data/data'] + } + } + + const state = { + ...initialState, + bulkUpload: { + ...initialState.bulkUpload, + pathsInProgress: [] + } + } + + // Act + const nextState = reducer(currentState, uploadDataBulkFailed('data/data')) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + customTutorials: nextState, + }, + }) + + expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state) + }) + }) + // thunks describe('fetchCustomTutorials', () => { @@ -415,4 +512,56 @@ describe('slices', () => { expect(mockedStore.getActions()).toEqual(expectedActions) }) }) + + describe('uploadDataBulkAction', () => { + it('succeed to upload data', async () => { + // Arrange + const instanceId = '1' + const path = 'data/data' + const data = {} + const responsePayload = { status: 200, data } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(uploadDataBulkAction(instanceId, path)) + + // Assert + const expectedActions = [ + uploadDataBulk(path), + uploadDataBulkSuccess(path), + addMessageNotification( + successMessages.UPLOAD_DATA_BULK(data as IBulkActionOverview, getFileNameFromPath(path)) + ) + ] + + expect(mockedStore.getActions()).toEqual(expectedActions) + }) + + it('failed to delete tutorial', async () => { + // Arrange + const instanceId = '1' + const path = 'data/data' + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(uploadDataBulkAction(instanceId, path)) + + // Assert + const expectedActions = [ + uploadDataBulk(path), + uploadDataBulkFailed(path), + addErrorNotification(responsePayload as AxiosError), + ] + + expect(mockedStore.getActions()).toEqual(expectedActions) + }) + }) }) diff --git a/redisinsight/ui/src/slices/workbench/wb-custom-tutorials.ts b/redisinsight/ui/src/slices/workbench/wb-custom-tutorials.ts index 6bf5db67b5..8992515a05 100644 --- a/redisinsight/ui/src/slices/workbench/wb-custom-tutorials.ts +++ b/redisinsight/ui/src/slices/workbench/wb-custom-tutorials.ts @@ -1,15 +1,19 @@ import { createSlice } from '@reduxjs/toolkit' import { remove } from 'lodash' +import { AxiosError } from 'axios' import { ApiEndpoints } from 'uiSrc/constants' -import { getApiErrorMessage, isStatusSuccessful, } from 'uiSrc/utils' +import { getApiErrorMessage, getUrl, isStatusSuccessful, } from 'uiSrc/utils' import { apiService } from 'uiSrc/services' import { EnablementAreaComponent, + IBulkActionOverview, IEnablementAreaItem, - StateWorkbenchEnablementArea, + StateWorkbenchCustomTutorials, } from 'uiSrc/slices/interfaces' -import { addErrorNotification } from 'uiSrc/slices/app/notifications' +import { addErrorNotification, addMessageNotification } from 'uiSrc/slices/app/notifications' +import successMessages from 'uiSrc/components/notifications/success-messages' +import { getFileNameFromPath } from 'uiSrc/utils/pathUtil' import { AppDispatch, RootState } from '../store' export const defaultItems: IEnablementAreaItem[] = [ @@ -21,11 +25,14 @@ export const defaultItems: IEnablementAreaItem[] = [ } ] -export const initialState: StateWorkbenchEnablementArea = { +export const initialState: StateWorkbenchCustomTutorials = { loading: false, deleting: false, error: '', items: defaultItems, + bulkUpload: { + pathsInProgress: [] + } } // A slice for recipes @@ -71,11 +78,21 @@ const workbenchCustomTutorialsSlice = createSlice({ state.deleting = false state.error = payload }, + uploadDataBulk: (state, { payload }) => { + state.bulkUpload.pathsInProgress.push(payload) + }, + uploadDataBulkSuccess: (state, { payload }) => { + remove(state.bulkUpload.pathsInProgress, (p) => p === payload) + }, + uploadDataBulkFailed: (state, { payload }) => { + remove(state.bulkUpload.pathsInProgress, (p) => p === payload) + } } }) // A selector export const workbenchCustomTutorialsSelector = (state: RootState) => state.workbench.customTutorials +export const customTutorialsBulkUploadSelector = (state: RootState) => state.workbench.customTutorials.bulkUpload // Actions generated from the slice export const { @@ -87,7 +104,10 @@ export const { uploadWBCustomTutorialFailure, deleteWbCustomTutorial, deleteWBCustomTutorialSuccess, - deleteWBCustomTutorialFailure + deleteWBCustomTutorialFailure, + uploadDataBulk, + uploadDataBulkSuccess, + uploadDataBulkFailed, } = workbenchCustomTutorialsSlice.actions // The reducer @@ -105,7 +125,7 @@ export function fetchCustomTutorials(onSuccessAction?: () => void, onFailAction? onSuccessAction?.() } } catch (error) { - const errorMessage = getApiErrorMessage(error) + const errorMessage = getApiErrorMessage(error as AxiosError) dispatch(getWBCustomTutorialsFailure(errorMessage)) onFailAction?.() } @@ -134,7 +154,8 @@ export function uploadCustomTutorial( dispatch(uploadWBCustomTutorialSuccess(data)) onSuccessAction?.() } - } catch (error) { + } catch (_error) { + const error = _error as AxiosError const errorMessage = getApiErrorMessage(error) dispatch(uploadWBCustomTutorialFailure(errorMessage)) dispatch(addErrorNotification(error)) @@ -153,9 +174,43 @@ export function deleteCustomTutorial(id: string, onSuccessAction?: () => void, o onSuccessAction?.() } } catch (error) { - const errorMessage = getApiErrorMessage(error) + const errorMessage = getApiErrorMessage(error as AxiosError) dispatch(deleteWBCustomTutorialFailure(errorMessage)) - dispatch(addErrorNotification(error)) + dispatch(addErrorNotification(error as AxiosError)) + onFailAction?.() + } + } +} + +export function uploadDataBulkAction( + instanceId: string, + path: string, + onSuccessAction?: () => void, + onFailAction?: () => void +) { + return async (dispatch: AppDispatch) => { + dispatch(uploadDataBulk(path)) + try { + const { status, data } = await apiService.post( + getUrl( + instanceId, + ApiEndpoints.BULK_ACTIONS_IMPORT_TUTORIAL_DATA + ), + { path }, + ) + + if (isStatusSuccessful(status)) { + dispatch(uploadDataBulkSuccess(path)) + dispatch( + addMessageNotification( + successMessages.UPLOAD_DATA_BULK(data as IBulkActionOverview, getFileNameFromPath(path)) + ) + ) + onSuccessAction?.() + } + } catch (error) { + dispatch(uploadDataBulkFailed(path)) + dispatch(addErrorNotification(error as AxiosError)) onFailAction?.() } } diff --git a/redisinsight/ui/src/styles/components/_toasts.scss b/redisinsight/ui/src/styles/components/_toasts.scss index 142023df1f..f59bb97844 100644 --- a/redisinsight/ui/src/styles/components/_toasts.scss +++ b/redisinsight/ui/src/styles/components/_toasts.scss @@ -54,7 +54,21 @@ } } +.euiGlobalToastList { + width: auto !important; + + .euiToast { + width: 340px !important; + + &.dynamic { + width: auto !important; + max-width: 480px !important; + } + } +} + .euiGlobalToastList--right:not(:empty) { + align-items: flex-end !important; right: 14px !important; } diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 2fc6614096..702c553183 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -118,6 +118,8 @@ export enum TelemetryEvent { WORKBENCH_ENABLEMENT_AREA_TUTORIAL_LINK_CHANGED = 'WORKBENCH_ENABLEMENT_AREA_TUTORIAL_LINK_CHANGED', WORKBENCH_ENABLEMENT_AREA_TUTORIAL_DELETED = 'WORKBENCH_ENABLEMENT_AREA_TUTORIAL_DELETED', WORKBENCH_ENABLEMENT_AREA_TUTORIAL_INFO_CLICKED = 'WORKBENCH_ENABLEMENT_AREA_TUTORIAL_INFO_CLICKED', + WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_CLICKED = 'WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_CLICKED', + WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_SUBMITTED = 'WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_SUBMITTED', PROFILER_OPENED = 'PROFILER_OPENED', PROFILER_STARTED = 'PROFILER_STARTED', diff --git a/redisinsight/ui/src/utils/pathUtil.ts b/redisinsight/ui/src/utils/pathUtil.ts index 5528d8ac96..ce2ee35671 100644 --- a/redisinsight/ui/src/utils/pathUtil.ts +++ b/redisinsight/ui/src/utils/pathUtil.ts @@ -37,3 +37,5 @@ export const prepareTutorialDataFileUrlFromMd = (nodeUrl: string, mdPath: string const pathUrl = new URL(mdPath, RESOURCES_BASE_URL) return new URL(nodeUrl, pathUrl).toString() } + +export const getFileNameFromPath = (path: string): string => path.split('/').pop() || '' From 89670ec7820eab474010e525ba2f8477dd194804 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 26 Apr 2023 15:03:39 +0200 Subject: [PATCH 08/32] #RI-4404 - fix test --- redisinsight/ui/src/utils/tests/pathUtil.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/utils/tests/pathUtil.spec.ts b/redisinsight/ui/src/utils/tests/pathUtil.spec.ts index 1177029eec..f9aacc55fb 100644 --- a/redisinsight/ui/src/utils/tests/pathUtil.spec.ts +++ b/redisinsight/ui/src/utils/tests/pathUtil.spec.ts @@ -23,7 +23,7 @@ const testCases = [ { url: '/_images/relative.png', path: '/unknown-path/lvl1/lvl2/lvl3/intro.md', - result: `${RESOURCES_BASE_URL}/unknown-path/lvl1/lvl2/lvl3/intro.md/_images/relative.png`, + result: `${RESOURCES_BASE_URL}unknown-path/lvl1/lvl2/lvl3/intro.md/_images/relative.png`, }, { url: 'https://somesite.test/image.png', From 78d13f9537f9a869bb9d88c785b7e34fdf5cae30 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 1 May 2023 10:40:58 +0200 Subject: [PATCH 09/32] #RI-4465 - fix multiple tooltips #RI-4464 - fix light theme styling --- .../notifications/styles.module.scss | 5 +++++ .../notifications/success-messages.tsx | 18 +++++++++--------- .../RedisUploadButton/RedisUploadButton.tsx | 17 ++++++++++------- .../RedisUploadButton/styles.module.scss | 6 +++++- .../themes/dark_theme/_dark_theme.lazy.scss | 1 + .../themes/light_theme/_light_theme.lazy.scss | 1 + 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/redisinsight/ui/src/components/notifications/styles.module.scss b/redisinsight/ui/src/components/notifications/styles.module.scss index 96f7f3fb34..49160a28d4 100644 --- a/redisinsight/ui/src/components/notifications/styles.module.scss +++ b/redisinsight/ui/src/components/notifications/styles.module.scss @@ -24,8 +24,13 @@ :global(.euiFlexItem:not(:last-child)) { margin-right: 14px !important; } + .summaryValue { font-size: 14px; font-weight: 500; } + + .summaryLabel { + color: var(--euiToastLightColor) !important; + } } diff --git a/redisinsight/ui/src/components/notifications/success-messages.tsx b/redisinsight/ui/src/components/notifications/success-messages.tsx index 9260e36f8c..88edef0a9e 100644 --- a/redisinsight/ui/src/components/notifications/success-messages.tsx +++ b/redisinsight/ui/src/components/notifications/success-messages.tsx @@ -163,7 +163,7 @@ export default { <> Action completed
- Data uploaded with file: {formatLongName(fileName, 24, 5)} + Data uploaded with file: {formatLongName(fileName, 24, 5)} ), message: ( @@ -175,20 +175,20 @@ export default { className={styles.summary} > - {numberWithSpaces(processed)} - Commands Processed + {numberWithSpaces(processed)} + Commands Processed - {numberWithSpaces(succeed)} - Success + {numberWithSpaces(succeed)} + Success - {numberWithSpaces(failed)} - Errors + {numberWithSpaces(failed)} + Errors - {millisecondsFormat(data?.duration || 0, 'H:mm:ss.SSS')} - Time Taken + {millisecondsFormat(data?.duration || 0, 'H:mm:ss.SSS')} + Time Taken ), diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx index 57588d8aee..b74e5d07a3 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/RedisUploadButton.tsx @@ -30,13 +30,16 @@ const RedisUploadButton = ({ label, path }: Props) => { }, [pathsInProgress]) const openPopover = () => { - setIsPopoverOpen(true) - sendEventTelemetry({ - event: TelemetryEvent.WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_CLICKED, - eventData: { - databaseId: instanceId - } - }) + if (!isPopoverOpen) { + sendEventTelemetry({ + event: TelemetryEvent.WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_CLICKED, + eventData: { + databaseId: instanceId + } + }) + } + + setIsPopoverOpen((v) => !v) } const uploadData = async () => { diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/styles.module.scss index f2b51a5640..56840b0e49 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/styles.module.scss +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/RedisUploadButton/styles.module.scss @@ -27,9 +27,13 @@ .panelPopover { border-color: var(--euiColorPrimary) !important; - :global(.euiPopover__panelArrow:before) { + :global(.euiPopover__panelArrow--bottom:before) { border-bottom-color: var(--euiColorPrimary) !important; } + + :global(.euiPopover__panelArrow--top:before) { + border-top-color: var(--euiColorPrimary) !important; + } } .popoverAnchor { diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss index 1f1ffb713c..8d5bb9be45 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss @@ -73,6 +73,7 @@ --euiToastSuccessBorderColor: #{$euiToastSuccessBorderColor}; --euiToastDangerBtnColor: #{$euiToastDangerBtnColor}; --euiToastDangerBorderColor: #{$euiToastDangerBorderColor}; + --euiToastLightColor: #{$euiColorDarkShade}; // Custom --htmlColor: #{$htmlColor}; diff --git a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss index 5fdef0de41..c6239f5017 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss @@ -74,6 +74,7 @@ --euiToastSuccessBorderColor: #{$euiToastSuccessBorderColor}; --euiToastDangerBtnColor: #{$euiToastDangerBtnColor}; --euiToastDangerBorderColor: #{$euiToastDangerBorderColor}; + --euiToastLightColor: #{$euiColorLightestShade}; // Custom --htmlColor: #{$htmlColor}; From 5c9c69731ad4acf7e9f1803c8e7e1de475e95434 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 2 May 2023 17:39:50 +0300 Subject: [PATCH 10/32] #RI-4466 fix file was not found on win --- .../bulk-actions/bulk-import.service.spec.ts | 20 +++++++++++-------- .../bulk-actions/bulk-import.service.ts | 17 +++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts index 8895a1d5ef..48b56afc8a 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts @@ -260,14 +260,19 @@ describe('BulkImportService', () => { expect(mockedFs.readFile).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, 'danger')); }); - it('should normalize path before importing and not search for file outside home folder (relative)', async () => { + it('should normalize path before importing and throw an error when search for file outside home folder (relative)', async () => { mockedFs.pathExists.mockImplementationOnce(async () => true); - await service.uploadFromTutorial(mockClientMetadata, { - path: '../../../danger', - }); + try { + await service.uploadFromTutorial(mockClientMetadata, { + path: '../../../danger', + }); - expect(mockedFs.readFile).toHaveBeenCalledWith(join(PATH_CONFIG.homedir, 'danger')); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + expect(e.message).toEqual('Data file was not found'); + } }); it('should throw BadRequest when no file found', async () => { @@ -289,9 +294,8 @@ describe('BulkImportService', () => { mockedFs.stat.mockImplementationOnce(async () => ({ size: 100 * 1024 * 1024 + 1 } as fs.Stats)); try { - await service.uploadFromTutorial(mockClientMetadata, { - path: '../../../danger', - }); + await service.uploadFromTutorial(mockClientMetadata, mockUploadImportFileByPathDto); + fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts index 9757e05231..039a6cbf59 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts @@ -155,21 +155,18 @@ export class BulkImportService { dto: UploadImportFileByPathDto, ): Promise { try { + const filePath = join(dto.path); + const staticPath = join(SERVER_CONFIG.base, SERVER_CONFIG.staticUri); - let trimmedPath = dto.path; - if (dto.path.indexOf(staticPath) === 0) { - trimmedPath = dto.path.slice(staticPath.length); + let trimmedPath = filePath; + if (filePath.indexOf(staticPath) === 0) { + trimmedPath = filePath.slice(staticPath.length); } - const resolvedPath = resolve( - '/', - trimmedPath, - ); - - const path = join(PATH_CONFIG.homedir, resolvedPath); + const path = join(PATH_CONFIG.homedir, trimmedPath); - if (!await fs.pathExists(path)) { + if (!path.startsWith(PATH_CONFIG.homedir) || !await fs.pathExists(path)) { throw new BadRequestException('Data file was not found'); } From beb2bf3cdb5cf9e9b4611ed74c1d4ea983844f55 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 3 May 2023 17:08:27 +0200 Subject: [PATCH 11/32] add tests for custom tutorial bulk upload --- tests/e2e/helpers/common.ts | 35 +++ tests/e2e/helpers/database.ts | 4 +- tests/e2e/package.json | 3 +- tests/e2e/pageObjects/base-page.ts | 2 + tests/e2e/pageObjects/browser-page.ts | 30 +-- .../myRedisDatabase/add-redis-database.ts | 1 - tests/e2e/pageObjects/components/toast.ts | 9 + .../pageObjects/my-redis-databases-page.ts | 11 +- tests/e2e/pageObjects/workbench-page.ts | 2 + .../upload-tutorials/customTutorials.zip | Bin 2271 -> 0 bytes .../customTutorials/_images/image.png | Bin 0 -> 45812 bytes .../_upload/bulkUplAllKeyTypes.txt | 9 + .../customTutorials/_upload/bulkUplString.txt | 1 + .../customTutorials/folder-1/probably-1.md | 60 +++++ .../customTutorials/folder-2/vector-2.md | 1 + .../critical-path/browser/bulk-upload.e2e.ts | 2 +- .../critical-path/browser/json-key.e2e.ts | 4 +- .../database/clone-databases.e2e.ts | 6 +- .../database/connecting-to-the-db.e2e.ts | 6 +- .../database/logical-databases.e2e.ts | 2 +- .../tests/regression/browser/add-keys.e2e.ts | 4 +- .../regression/browser/consumer-group.e2e.ts | 2 +- .../browser/keys-all-databases.e2e.ts | 2 +- .../regression/browser/upload-json-key.e2e.ts | 2 +- .../workbench/import-tutorials.e2e.ts | 208 ++++++++++++------ tests/e2e/tests/smoke/browser/add-keys.e2e.ts | 12 +- .../e2e/tests/smoke/browser/hash-field.e2e.ts | 4 +- tests/e2e/tests/smoke/browser/json-key.e2e.ts | 2 +- tests/e2e/tests/smoke/browser/list-key.e2e.ts | 4 +- .../browser/list-of-keys-verifications.e2e.ts | 8 +- tests/e2e/tests/smoke/browser/set-key.e2e.ts | 2 +- .../smoke/browser/verify-keys-refresh.e2e.ts | 2 +- tests/e2e/tests/smoke/browser/zset-key.e2e.ts | 4 +- .../smoke/database/add-standalone-db.e2e.ts | 2 +- tests/e2e/yarn.lock | 14 ++ 35 files changed, 332 insertions(+), 128 deletions(-) create mode 100644 tests/e2e/pageObjects/components/toast.ts delete mode 100644 tests/e2e/test-data/upload-tutorials/customTutorials.zip create mode 100644 tests/e2e/test-data/upload-tutorials/customTutorials/_images/image.png create mode 100644 tests/e2e/test-data/upload-tutorials/customTutorials/_upload/bulkUplAllKeyTypes.txt create mode 100644 tests/e2e/test-data/upload-tutorials/customTutorials/_upload/bulkUplString.txt create mode 100644 tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md create mode 100644 tests/e2e/test-data/upload-tutorials/customTutorials/folder-2/vector-2.md diff --git a/tests/e2e/helpers/common.ts b/tests/e2e/helpers/common.ts index 2d992c8f10..1d4dc9cde1 100644 --- a/tests/e2e/helpers/common.ts +++ b/tests/e2e/helpers/common.ts @@ -1,3 +1,6 @@ +import * as path from 'path'; +import * as archiver from 'archiver'; +import * as fs from 'fs'; import { ClientFunction, RequestMock, t } from 'testcafe'; import { Chance } from 'chance'; import { apiUrl, commonUrl } from './conf'; @@ -188,4 +191,36 @@ export class Common { static async getPageUrl(): Promise { return (await ClientFunction(() => window.location.href))(); } + + /** + * Create Zip archive from folder + * @param folderPath Path to folder to archive + * @param zipName Zip archive name + */ + static async createZipFromFolder(folderPath: string, zipName: string): Promise { + const sourceDir = path.join(__dirname, folderPath); + const zipFilePath = path.join(__dirname, zipName); + console.log(sourceDir); + console.log(zipFilePath); + const output = fs.createWriteStream(zipFilePath); + const archive = archiver('zip', { zlib: { level: 9 } }); + + // Add the contents of the directory to the zip archive + archive.directory(sourceDir, false); + + // Finalize the archive and write it to disk + archive.finalize(); + await new Promise((resolve) => { + output.on('close', resolve); + archive.pipe(output); + }); + } + + /** + * Delete file from folder + * @param folderPath Path to file + */ + static async deleteFileFromFolder(filePath: string): Promise { + fs.unlinkSync(path.join(__dirname, filePath)); + } } diff --git a/tests/e2e/helpers/database.ts b/tests/e2e/helpers/database.ts index e69f5bbc28..17855e4fec 100644 --- a/tests/e2e/helpers/database.ts +++ b/tests/e2e/helpers/database.ts @@ -28,7 +28,7 @@ export async function addNewStandaloneDatabase(databaseParameters: AddNewDatabas // Wait for database to be exist .expect(myRedisDatabasePage.dbNameList.withExactText(databaseParameters.databaseName ?? '').exists).ok('The database not displayed', { timeout: 10000 }) // Close message - .click(myRedisDatabasePage.toastCloseButton); + .click(myRedisDatabasePage.Toast.toastCloseButton); } /** @@ -77,7 +77,7 @@ export async function addOSSClusterDatabase(databaseParameters: OSSClusterParame await t .click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton) // Check for info message that DB was added - .expect(myRedisDatabasePage.databaseInfoMessage.exists).ok('Info message not exists', { timeout: 10000 }) + .expect(myRedisDatabasePage.Toast.toastHeader.exists).ok('Info message not exists', { timeout: 10000 }) // Wait for database to be exist .expect(myRedisDatabasePage.dbNameList.withExactText(databaseParameters.ossClusterDatabaseName).exists).ok('The database not displayed', { timeout: 10000 }); } diff --git a/tests/e2e/package.json b/tests/e2e/package.json index bab8879145..a9921047ec 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -26,6 +26,7 @@ "cli-argument-parser": "0.4.5" }, "devDependencies": { + "@types/archiver": "^5.3.2", "@types/chance": "1.1.3", "@types/edit-json-file": "1.7.0", "@types/supertest": "^2.0.8", @@ -37,8 +38,8 @@ "edit-json-file": "1.7.0", "eslint": "7.32.0", "eslint-plugin-import": "2.24.2", - "sqlite3": "5.0.10", "redis": "3.0.2", + "sqlite3": "5.0.10", "supertest": "^4.0.2", "testcafe": "1.14.2", "testcafe-browser-provider-electron": "0.0.18", diff --git a/tests/e2e/pageObjects/base-page.ts b/tests/e2e/pageObjects/base-page.ts index 98e35a1390..b86eb49302 100644 --- a/tests/e2e/pageObjects/base-page.ts +++ b/tests/e2e/pageObjects/base-page.ts @@ -1,8 +1,10 @@ import { t } from 'testcafe'; import { NavigationPanel } from './components/navigation-panel'; +import { Toast } from './components/toast'; export class BasePage { NavigationPanel = new NavigationPanel(); + Toast = new Toast(); /** * Reload page diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index de94dd9bf0..cea7a13c09 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -61,7 +61,6 @@ export class BrowserPage extends InstancePage { addJsonObjectButton = Selector('[data-testid=add-object-btn]'); addJsonFieldButton = Selector('[data-testid=add-field-btn]'); expandJsonObject = Selector('[data-testid=expand-object]'); - toastCloseButton = Selector('[data-test-subj=toastCloseButton]'); scoreButton = Selector('[data-testid=score-button]'); sortingButton = Selector('[data-testid=header-sorting-button]'); editJsonObjectButton = Selector('[data-testid=edit-object-btn]'); @@ -198,7 +197,6 @@ export class BrowserPage extends InstancePage { //TEXT ELEMENTS keySizeDetails = Selector('[data-testid=key-size-text]'); keyLengthDetails = Selector('[data-testid=key-length-text]'); - notificationMessage = Selector('[data-test-subj=euiToastHeader]'); keyNameInTheList = Selector(this.cssSelectorKey); databaseNames = Selector('[data-testid^=db_name_]'); hashFieldsList = Selector('[data-testid^=hash-field-] span'); @@ -265,7 +263,6 @@ export class BrowserPage extends InstancePage { streamConsumerName = Selector('[data-testid^=stream-consumer-]'); consumerGroup = Selector('[data-testid^=stream-group-]'); entryIdInfoIcon = Selector('[data-testid=entry-id-info-icon]'); - errorMessage = Selector('[data-test-subj=toast-error]'); entryIdError = Selector('[data-testid=id-error]'); pendingCount = Selector('[data-testid=pending-count]'); lastRefreshMessage = Selector('[data-testid=refresh-message]'); @@ -442,7 +439,7 @@ export class BrowserPage extends InstancePage { await t.typeText(this.streamValue, value, { replace: true, paste: true }); await t.expect(this.addKeyButton.withAttribute('disabled').exists).notOk('Add Key button not clickable'); await t.click(this.addKeyButton); - await t.click(this.toastCloseButton); + await t.click(this.Toast.toastCloseButton); } /** @@ -545,15 +542,10 @@ export class BrowserPage extends InstancePage { return keyNameInTheList.exists; } - //Getting the text of the Notification message - async getMessageText(): Promise { - return this.notificationMessage.textContent; - } - //Delete key from details async deleteKey(): Promise { - if (await this.toastCloseButton.exists) { - await t.click(this.toastCloseButton); + if (await this.Toast.toastCloseButton.exists) { + await t.click(this.Toast.toastCloseButton); } await t.click(this.keyNameInTheList); await t.click(this.deleteKeyButton); @@ -640,8 +632,8 @@ export class BrowserPage extends InstancePage { * @param keyValue The hash value */ async addFieldToHash(keyFieldValue: string, keyValue: string): Promise { - if (await this.toastCloseButton.exists) { - await t.click(this.toastCloseButton); + if (await this.Toast.toastCloseButton.exists) { + await t.click(this.Toast.toastCloseButton); } await t.click(this.addKeyValueItemsButton); await t.typeText(this.hashFieldInput, keyFieldValue, { replace: true, paste: true }); @@ -734,8 +726,8 @@ export class BrowserPage extends InstancePage { * @param keyMember The value of the set member */ async addMemberToSet(keyMember: string): Promise { - if (await this.toastCloseButton.exists) { - await t.click(this.toastCloseButton); + if (await this.Toast.toastCloseButton.exists) { + await t.click(this.Toast.toastCloseButton); } await t .click(this.addKeyValueItemsButton) @@ -749,8 +741,8 @@ export class BrowserPage extends InstancePage { * @param score The value of the score */ async addMemberToZSet(keyMember: string, score: string): Promise { - if (await this.toastCloseButton.exists) { - await t.click(this.toastCloseButton); + if (await this.Toast.toastCloseButton.exists) { + await t.click(this.Toast.toastCloseButton); } await t .click(this.addKeyValueItemsButton) @@ -784,8 +776,8 @@ export class BrowserPage extends InstancePage { * @param element The value of the list element */ async addElementToList(element: string): Promise { - if (await this.toastCloseButton.exists) { - await t.click(this.toastCloseButton); + if (await this.Toast.toastCloseButton.exists) { + await t.click(this.Toast.toastCloseButton); } await t .click(this.addKeyValueItemsButton) diff --git a/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts b/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts index bccbab4391..75539b2f7d 100644 --- a/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts +++ b/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts @@ -40,7 +40,6 @@ export class AddRedisDatabase { secretKeyInput = Selector('[data-testid=secret-key]'); welcomePageTitle = Selector('[data-testid=welcome-page-title]'); databaseIndexInput = Selector('[data-testid=db]'); - errorMessage = Selector('[data-test-subj=toast-error]'); databaseIndexMessage = Selector('[data-testid=db-index-message]'); primaryGroupNameInput = Selector('[data-testid=primary-group]'); masterGroupPassword = Selector('[data-testid=sentinel-master-password]'); diff --git a/tests/e2e/pageObjects/components/toast.ts b/tests/e2e/pageObjects/components/toast.ts new file mode 100644 index 0000000000..fe5cc0c9bc --- /dev/null +++ b/tests/e2e/pageObjects/components/toast.ts @@ -0,0 +1,9 @@ +import { Selector } from 'testcafe'; + +export class Toast { + toastHeader = Selector('[data-test-subj=euiToastHeader]'); + toastBody = Selector('[class*=euiToastBody]'); + toastSuccess = Selector('[class*=euiToast--success]'); + toastError = Selector('[class*=euiToast--danger]'); + toastCloseButton = Selector('[data-test-subj=toastCloseButton]'); +} diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index e648cbd3d3..0ca1642ee3 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -18,7 +18,7 @@ export class MyRedisDatabasePage extends BasePage { //BUTTONS deleteDatabaseButton = Selector('[data-testid^=delete-instance-]'); confirmDeleteButton = Selector('[data-testid^=delete-instance-]').withExactText('Remove'); - toastCloseButton = Selector('[data-test-subj=toastCloseButton]'); + deleteButtonInPopover = Selector('#deletePopover button'); confirmDeleteAllDbButton = Selector('[data-testid=delete-selected-dbs]'); editDatabaseButton = Selector('[data-testid^=edit-instance]'); @@ -60,7 +60,6 @@ export class MyRedisDatabasePage extends BasePage { moduleQuantifier = Selector('[data-testid=_module]'); dbNameList = Selector('[data-testid^=instance-name]', { timeout: 3000 }); tableRowContent = Selector('[data-test-subj=database-alias-column]'); - databaseInfoMessage = Selector('[data-test-subj=euiToastHeader]'); hostPort = Selector('[data-testid=host-port]'); noResultsFoundMessage = Selector('div').withExactText('No results found'); noResultsFoundText = Selector('div').withExactText('No databases matched your search. Try reducing the criteria.'); @@ -79,8 +78,8 @@ export class MyRedisDatabasePage extends BasePage { * @param dbName The name of the database to be opened */ async clickOnDBByName(dbName: string): Promise { - if (await this.toastCloseButton.exists) { - await t.click(this.toastCloseButton); + if (await this.Toast.toastCloseButton.exists) { + await t.click(this.Toast.toastCloseButton); } const db = this.dbNameList.withExactText(dbName.trim()); await t.expect(db.exists).ok(`"${dbName}" database doesn't exist`, {timeout: 10000}); @@ -103,8 +102,8 @@ export class MyRedisDatabasePage extends BasePage { .click(this.deleteDatabaseButton) .click(this.confirmDeleteButton); } - if (await this.toastCloseButton.exists) { - await t.click(this.toastCloseButton); + if (await this.Toast.toastCloseButton.exists) { + await t.click(this.Toast.toastCloseButton); } } diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index f4cb0f5b8e..5d430a893e 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -80,6 +80,8 @@ export class WorkbenchPage extends InstancePage { copyCommand = Selector('[data-testid=copy-command]'); redisStackTimeSeriesLoadMorePoints = Selector('[data-testid=preselect-Load more data points]'); documentHashCreateButton = Selector('[data-testid=preselect-auto-Create]'); + uploadDataBulkBtn = Selector('[data-testid=upload-data-bulk-btn]'); + uploadDataBulkApplyBtn = Selector('[data-testid=upload-data-bulk-apply-btn]'); //ICONS noCommandHistoryIcon = Selector('[data-testid=wb_no-results__icon]'); parametersAnchor = Selector('[data-testid=parameters-anchor]'); diff --git a/tests/e2e/test-data/upload-tutorials/customTutorials.zip b/tests/e2e/test-data/upload-tutorials/customTutorials.zip deleted file mode 100644 index c71a78c43705fe06a092b419ce5a9d18ce9c49d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2271 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�>xE&<%o?tsq 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/test-data/upload-tutorials/customTutorials/_upload/bulkUplAllKeyTypes.txt b/tests/e2e/test-data/upload-tutorials/customTutorials/_upload/bulkUplAllKeyTypes.txt new file mode 100644 index 0000000000..3b71461d24 --- /dev/null +++ b/tests/e2e/test-data/upload-tutorials/customTutorials/_upload/bulkUplAllKeyTypes.txt @@ -0,0 +1,9 @@ +HSET hashkey1 'field' 'value' +LPUSH listkey1 'element' +SADD setkey1 'member' +ZADD zsetkey1 1 'member' +SET stringkey1 'value' +JSON.SET jsonkey1 . '1' +XADD streamkey1 * 'field' 'value' +GRAPH.QUERY graphkey1 "CREATE ()" +TS.CREATE tskey1 \ No newline at end of file diff --git a/tests/e2e/test-data/upload-tutorials/customTutorials/_upload/bulkUplString.txt b/tests/e2e/test-data/upload-tutorials/customTutorials/_upload/bulkUplString.txt new file mode 100644 index 0000000000..1c94a82bcf --- /dev/null +++ b/tests/e2e/test-data/upload-tutorials/customTutorials/_upload/bulkUplString.txt @@ -0,0 +1 @@ +SET stringkey1test 'value' \ No newline at end of file diff --git a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md new file mode 100644 index 0000000000..378d612d36 --- /dev/null +++ b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md @@ -0,0 +1,60 @@ +In very broad terms probabilistic data structures (PDS) allow us to get to a "close enough" result in a much shorter time and by using significantly less memory. + +External: + +![RedisInsight screen external](https://github.com/RedisInsight/RedisInsight/blob/main/.github/redisinsight_browser.png?raw=true) + +Relative: + +![RedisInsight screen relative](../_images/image.png) + +Relative path button: + +```redis-upload:[../../_upload/bulkUplAllKeyTypes.txt] Upload relative +``` + +Relative path long name button: + +```redis-upload:[../../_upload/bulkUplAllKeyTypes.txt] Longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname +``` + +Absolute path button: + +```redis-upload:[/_upload/bulkUplString.txt] Upload absolute +``` + +Invalid absolute path button: + +```redis-upload:[/_upload/bulkUplAllKeyTypes] Invalid absolute +``` + +Invalid relative path button: + +```redis-upload:[../_upload/bulkUplAllKeyTypes.txt] Invalid relative +``` + +Redis Stack supports 4 of the most famous PDS: +- Bloom filters +- Cuckoo filters +- Count-Min Sketch +- Top-K + +In the rest of this tutorial we'll introduce how you can use a Bloom filter to save many heavy calls to the relational database, or a lot of memory, compared to using sets or hashes. +A Bloom filter is a probabilistic data structure that enables you to check if an element is present in a set using a very small memory space of a fixed size. **It can guarantee the absence of an element from a set, but it can only give an estimation about its presence**. So when it responds that an element is not present in a set (a negative answer), you can be sure that indeed is the case. However, one out of every N positive answers will be wrong. +Even though it looks unusual at a first glance, this kind of uncertainty still has its place in computer science. There are many cases out there where a negative answer will prevent very costly operations; + +How can a Bloom filter be useful to our bike shop? For starters, we could keep a Bloom filter that stores all usernames of people who've already registered with our service. That way, when someone is creating a new account we can very quickly check if that username is free. If the answer is yes, we'd still have to go and check the main database for the precise result, but if the answer is no, we can skip that call and continue with the registration. + +Another, perhaps more interesting example is for showing better and more relevant ads to users. We could keep a bloom filter per user with all the products they've bought from the shop, and when we get a list of products from our suggestion engine we could check it against this filter. + + +```redis Add all bought product ids in the Bloom filter +BF.MADD user:778:bought_products 4545667 9026875 3178945 4848754 1242449 +``` + +Just before we try to show an ad to a user, we can first check if that product id is already in their "bought products" Bloom filter. If the answer is yes - we might choose to check the main database, or we might skip to the next recommendation from our list. But if the answer is no, then we know for sure that our user hasn't bought that product: + +```redis Has a user bought this product? +BF.EXISTS user:778:bought_products 1234567 // No, the user has not bought this product +BF.EXISTS user:778:bought_products 3178945 // The user might have bought this product +``` diff --git a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-2/vector-2.md b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-2/vector-2.md new file mode 100644 index 0000000000..eb64bd0b91 --- /dev/null +++ b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-2/vector-2.md @@ -0,0 +1 @@ +In very broad terms probabilistic data structures (PDS) allow us to get to a "close enough" result in a much shorter time and by using significantly less memory. \ No newline at end of file diff --git a/tests/e2e/tests/critical-path/browser/bulk-upload.e2e.ts b/tests/e2e/tests/critical-path/browser/bulk-upload.e2e.ts index ec0da5b1dd..22c2726be2 100644 --- a/tests/e2e/tests/critical-path/browser/bulk-upload.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/bulk-upload.e2e.ts @@ -20,6 +20,7 @@ const verifyCompletedResultText = async(resultsText: string[]): Promise => for (const result of resultsText) { await t.expect(browserPage.BulkActions.bulkUploadCompletedSummary.textContent).contains(result, 'Bulk upload completed summary not correct'); } + await t.expect(browserPage.BulkActions.bulkUploadCompletedSummary.textContent).notContains('0:00:00.00', 'Bulk upload Time taken not correct'); }; fixture `Bulk Upload` @@ -49,7 +50,6 @@ test('Verify bulk upload of different text docs formats', async t => { // Verify that keys of all types can be uploaded await browserPage.BulkActions.uploadFileInBulk(filePathes.allKeysFile); await verifyCompletedResultText(allKeysResults); - await t.expect(browserPage.BulkActions.bulkUploadCompletedSummary.textContent).notContains('0:00:00.00', 'Bulk upload completed summary not correct'); await browserPage.searchByKeyName('*key1'); await verifyKeysDisplayedInTheList(keyNames); diff --git a/tests/e2e/tests/critical-path/browser/json-key.e2e.ts b/tests/e2e/tests/critical-path/browser/json-key.e2e.ts index 6707c5455d..1a86f5b0d1 100644 --- a/tests/e2e/tests/critical-path/browser/json-key.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/json-key.e2e.ts @@ -27,9 +27,9 @@ test('Verify that user can not add invalid JSON structure inside of created JSON // Add Json key with json object await browserPage.addJsonKey(keyName, value, keyTTL); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The notification'); - await t.click(browserPage.toastCloseButton); + await t.click(browserPage.Toast.toastCloseButton); // Add key with value on the same level await browserPage.addJsonKeyOnTheSameLevel('"key1"', '{}'); // Add invalid JSON structure diff --git a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts index 145f6bfd6a..bb8ea4c45c 100644 --- a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts @@ -35,7 +35,7 @@ test // Verify that user can test Standalone connection on edit and see the success message await t.click(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); - await t.expect(myRedisDatabasePage.databaseInfoMessage.textContent).contains('Connection is successful', 'Standalone connection is not successful'); + await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Connection is successful', 'Standalone connection is not successful'); // Verify that user can cancel the Clone by clicking the “Cancel” or the “x” button await t.click(myRedisDatabasePage.AddRedisDatabase.cloneDatabaseButton); @@ -75,7 +75,7 @@ test // Verify that user can test OSS Cluster connection on edit and see the success message await t.click(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); - await t.expect(myRedisDatabasePage.databaseInfoMessage.textContent).contains('Connection is successful', 'OSS Cluster connection is not successful'); + await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Connection is successful', 'OSS Cluster connection is not successful'); await t.click(myRedisDatabasePage.AddRedisDatabase.cloneDatabaseButton); await t @@ -109,7 +109,7 @@ test // Verify that user can test Sentinel connection on edit and see the success message await t.click(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); - await t.expect(myRedisDatabasePage.databaseInfoMessage.textContent).contains('Connection is successful', 'Sentinel connection is not successful'); + await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Connection is successful', 'Sentinel connection is not successful'); // Verify that for Sentinel Host and Port fields are replaced with editable Primary Group Name field await t diff --git a/tests/e2e/tests/critical-path/database/connecting-to-the-db.e2e.ts b/tests/e2e/tests/critical-path/database/connecting-to-the-db.e2e.ts index f64649c5b4..9c65a48f75 100644 --- a/tests/e2e/tests/critical-path/database/connecting-to-the-db.e2e.ts +++ b/tests/e2e/tests/critical-path/database/connecting-to-the-db.e2e.ts @@ -45,13 +45,13 @@ test // Verify that when user request to test database connection is not successfull, can see standart connection error await t.click(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); - await t.expect(myRedisDatabasePage.databaseInfoMessage.textContent).contains('Error', 'Invalid connection has no error on test'); + await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Error', 'Invalid connection has no error on test'); // Click for saving await t.click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); // Verify that the database is not in the list - await t.expect(myRedisDatabasePage.AddRedisDatabase.errorMessage.textContent).contains('Error', 'Error message not displayed', { timeout: 10000 }); - await t.expect(myRedisDatabasePage.AddRedisDatabase.errorMessage.textContent).contains(errorMessage, 'Error message not displayed', { timeout: 10000 }); + await t.expect(myRedisDatabasePage.Toast.toastError.textContent).contains('Error', 'Error message not displayed', { timeout: 10000 }); + await t.expect(myRedisDatabasePage.Toast.toastError.textContent).contains(errorMessage, 'Error message not displayed', { timeout: 10000 }); }); test .meta({ rte: rte.none })('Fields to add database prepopulation', async t => { diff --git a/tests/e2e/tests/critical-path/database/logical-databases.e2e.ts b/tests/e2e/tests/critical-path/database/logical-databases.e2e.ts index 40e4b99e8d..6bb364fd10 100644 --- a/tests/e2e/tests/critical-path/database/logical-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/logical-databases.e2e.ts @@ -22,7 +22,7 @@ test('Verify that user can add DB with logical index via host and port from Add // Verify that user can test database connection and see success message await t.click(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); - await t.expect(myRedisDatabasePage.databaseInfoMessage.textContent).contains('Connection is successful', 'Standalone connection is not successful'); + await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Connection is successful', 'Standalone connection is not successful'); // Enter logical index await t.click(myRedisDatabasePage.AddRedisDatabase.databaseIndexCheckbox); diff --git a/tests/e2e/tests/regression/browser/add-keys.e2e.ts b/tests/e2e/tests/regression/browser/add-keys.e2e.ts index 109cbd416d..c042d11157 100644 --- a/tests/e2e/tests/regression/browser/add-keys.e2e.ts +++ b/tests/e2e/tests/regression/browser/add-keys.e2e.ts @@ -33,8 +33,8 @@ test('Verify that user can create different types(string, number, null, array, b for (let i = 0; i < jsonKeys.length; i++) { const keySelector = await browserPage.getKeySelectorByName(jsonKeys[i][0]); await browserPage.addJsonKey(jsonKeys[i][0], jsonKeys[i][1]); - await t.hover(browserPage.toastCloseButton); - await t.click(browserPage.toastCloseButton); + await t.hover(browserPage.Toast.toastCloseButton); + await t.click(browserPage.Toast.toastCloseButton); await t.click(browserPage.refreshKeysButton); await t.expect(keySelector.exists).ok(`${jsonKeys[i][0]} key not displayed`); // Add additional check for array elements diff --git a/tests/e2e/tests/regression/browser/consumer-group.e2e.ts b/tests/e2e/tests/regression/browser/consumer-group.e2e.ts index f067d17865..b3a0b6edff 100644 --- a/tests/e2e/tests/regression/browser/consumer-group.e2e.ts +++ b/tests/e2e/tests/regression/browser/consumer-group.e2e.ts @@ -61,7 +61,7 @@ test('Verify that when user enter invalid Group Name the error message appears', // Verify the error message await t.click(browserPage.streamTabGroups); await browserPage.createConsumerGroup(consumerGroupName); - await t.expect(browserPage.errorMessage.textContent).contains(error, 'The error message that the Group name already exists not displayed'); + await t.expect(browserPage.Toast.toastError.textContent).contains(error, 'The error message that the Group name already exists not displayed'); }); test('Verify that user can sort Consumer Group column: A>Z / Z>A(A>Z is default table sorting)', async t => { keyName = Common.generateWord(20); diff --git a/tests/e2e/tests/regression/browser/keys-all-databases.e2e.ts b/tests/e2e/tests/regression/browser/keys-all-databases.e2e.ts index 5d73e726c2..642721dc1f 100644 --- a/tests/e2e/tests/regression/browser/keys-all-databases.e2e.ts +++ b/tests/e2e/tests/regression/browser/keys-all-databases.e2e.ts @@ -35,7 +35,7 @@ const verifyKeysAdded = async(): Promise => { // Add Hash key await browserPage.addHashKey(keyName); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The notification not correct'); // Check that new key is displayed in the list await browserPage.searchByKeyName(keyName); diff --git a/tests/e2e/tests/regression/browser/upload-json-key.e2e.ts b/tests/e2e/tests/regression/browser/upload-json-key.e2e.ts index f5f615ba30..1f6d4438e7 100644 --- a/tests/e2e/tests/regression/browser/upload-json-key.e2e.ts +++ b/tests/e2e/tests/regression/browser/upload-json-key.e2e.ts @@ -34,7 +34,7 @@ test('Verify that user can insert a JSON from .json file on the form to add a JS await t.typeText(browserPage.addKeyNameInput, keyName, { replace: true, paste: true }); await t.setFilesToUpload(browserPage.jsonUploadInput, [filePath]); await t.click(browserPage.addKeyButton); - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The key added notification not found'); // Verify that user can see the JSON value populated from the file when the insert is successful. for (const el of jsonValues) { diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index b24343b184..70b062b423 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -1,22 +1,32 @@ import * as path from 'path'; +import { t } from 'testcafe'; import { rte } from '../../../helpers/constants'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; -import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; +import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; +import { deleteAllKeysFromDB, verifyKeysDisplayedInTheList } from '../../../helpers/keys'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); -const filePath = path.join('..', '..', '..', 'test-data', 'upload-tutorials', 'customTutorials.zip'); -const tutorialName = 'customTutorials'; -const tutorialName2 = 'tutorialTestByLink'; -const link = 'https://drive.google.com/uc?id=1puRUoT8HmyZCekkeWNxBzXe_48TzXcJc&export=download'; -let folder1 = 'folder-1'; -let folder2 = 'folder-2'; +const browserPage = new BrowserPage(); +const zipFolderName = 'customTutorials'; +const folderPath = path.join('..', 'test-data', 'upload-tutorials', zipFolderName); +const folder1 = 'folder-1'; +const folder2 = 'folder-2'; +const internalLinkName2 = 'vector-2'; +let tutorialName = `${zipFolderName}${Common.generateWord(5)}`; +let zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`); let internalLinkName1 = 'probably-1'; -let internalLinkName2 = 'vector-2'; +const verifyCompletedResultText = async(resultsText: string[]): Promise => { + for (const result of resultsText) { + await t.expect(workbenchPage.Toast.toastBody.textContent).contains(result, 'Bulk upload completed summary not correct'); + } + await t.expect(workbenchPage.Toast.toastBody.textContent).notContains('0:00:00.00', 'Bulk upload Time taken not correct'); +}; fixture `Upload custom tutorials` .meta({ type: 'regression', rte: rte.standalone }) @@ -25,78 +35,90 @@ fixture `Upload custom tutorials` await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) - .afterEach(async () => { + .afterEach(async() => { await deleteStandaloneDatabaseApi(ossStandaloneConfig); }); /* https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4198, https://redislabs.atlassian.net/browse/RI-4302, https://redislabs.atlassian.net/browse/RI-4318 */ -test('Verify that user can upload tutorial with local zip file without manifest.json', async t => { - // Verify that user can upload custom tutorials on docker version - folder1 = 'folder-1'; - folder2 = 'folder-2'; - internalLinkName1 = 'probably-1'; - internalLinkName2 = 'vector-2'; +test + .before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + tutorialName = `${zipFolderName}${Common.generateWord(5)}`; + zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`); + // Create zip file for uploading + await Common.createZipFromFolder(folderPath, zipFilePath); + }) + .after(async() => { + // Delete zip file + await Common.deleteFileFromFolder(zipFilePath); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Verify that user can upload tutorial with local zip file without manifest.json', async t => { + // Verify that user can upload custom tutorials on docker version + internalLinkName1 = 'probably-1'; + const imageExternalPath = 'RedisInsight screen external'; + const imageRelativePath = 'RedisInsight screen relative'; - // Verify that user can see the “MY TUTORIALS” section in the Enablement area. - await t.expect(workbenchPage.customTutorials.visible).ok('custom tutorials sections is not visible'); - await t.click(workbenchPage.tutorialOpenUploadButton); - await t.expect(workbenchPage.tutorialSubmitButton.hasAttribute('disabled')).ok('submit button is not disabled'); + // Verify that user can see the “MY TUTORIALS” section in the Enablement area. + await t.expect(workbenchPage.customTutorials.visible).ok('custom tutorials sections is not visible'); + await t.click(workbenchPage.tutorialOpenUploadButton); + await t.expect(workbenchPage.tutorialSubmitButton.hasAttribute('disabled')).ok('submit button is not disabled'); - // Verify that User can request to add a new custom Tutorial by uploading a .zip archive from a local folder - await t.setFilesToUpload(workbenchPage.tutorialImport, [filePath]); - await t.click(workbenchPage.tutorialSubmitButton); - await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).visible).ok(`${tutorialName} tutorial is not uploaded`); + // Verify that User can request to add a new custom Tutorial by uploading a .zip archive from a local folder + await t.setFilesToUpload(workbenchPage.tutorialImport, [zipFilePath]); + await t.click(workbenchPage.tutorialSubmitButton); + await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).visible).ok(`${tutorialName} tutorial is not uploaded`); - // Verify that when user upload a .zip archive without a .json manifest, all markdown files are inserted at the same hierarchy level - await t.click(workbenchPage.tutorialAccordionButton.withText(tutorialName)); - await t.expect((await workbenchPage.getAccordionButtonWithName(folder1)).visible).ok(`${folder1} is not visible`); - await t.expect((await workbenchPage.getAccordionButtonWithName(folder2)).visible).ok(`${folder2} is not visible`); - await t.click(await workbenchPage.getAccordionButtonWithName(folder1)); - await t.expect((await workbenchPage.getInternalLinkWithManifest(internalLinkName1)).visible) - .ok(`${internalLinkName1} is not visible`); - await t.click(await workbenchPage.getAccordionButtonWithName(folder2)); - await t.expect((await workbenchPage.getInternalLinkWithManifest(internalLinkName2)).visible) - .ok(`${internalLinkName2} is not visible`); - await t.expect(workbenchPage.scrolledEnablementArea.exists).notOk('enablement area is visible before clicked'); - await t.click((await workbenchPage.getInternalLinkWithManifest(internalLinkName1))); - await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked'); + // Verify that when user upload a .zip archive without a .json manifest, all markdown files are inserted at the same hierarchy level + await t.click(workbenchPage.tutorialAccordionButton.withText(tutorialName)); + await t.expect((await workbenchPage.getAccordionButtonWithName(folder1)).visible).ok(`${folder1} is not visible`); + await t.expect((await workbenchPage.getAccordionButtonWithName(folder2)).visible).ok(`${folder2} is not visible`); + await t.click(await workbenchPage.getAccordionButtonWithName(folder1)); + await t.expect((await workbenchPage.getInternalLinkWithManifest(internalLinkName1)).visible) + .ok(`${internalLinkName1} is not visible`); + await t.click(await workbenchPage.getAccordionButtonWithName(folder2)); + await t.expect((await workbenchPage.getInternalLinkWithManifest(internalLinkName2)).visible) + .ok(`${internalLinkName2} is not visible`); + await t.expect(workbenchPage.scrolledEnablementArea.exists).notOk('enablement area is visible before clicked'); + await t.click((await workbenchPage.getInternalLinkWithManifest(internalLinkName1))); + await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked'); - // Error when github upload .zip with relative path in .md - // const imageExternalPath = 'RedisInsight screen external'; - // const imageRelativePath = 'RedisInsight screen relative'; - // Verify that user can see image in custom tutorials by providing absolute external path in md file - // const imageExternal = await workbenchPage.getTutorialImageByAlt(imageExternalPath); - // await workbenchPage.waitUntilImageRendered(imageExternal); - // const imageExternalHeight = await imageExternal.getStyleProperty('height'); - // await t.expect(parseInt(imageExternalHeight.replace(/[^\d]/g, ''))).gte(150); - // Verify that user can see image in custom tutorials by providing relative path in md file - // const imageRelative = await workbenchPage.getTutorialImageByAlt(imageRelativePath); - // await workbenchPage.waitUntilImageRendered(imageRelative); - // const imageRelativeHeight = await imageRelative.getStyleProperty('height'); - // await t.expect(parseInt(imageRelativeHeight.replace(/[^\d]/g, ''))).gte(150); + // Verify that user can see image in custom tutorials by providing absolute external path in md file + const imageExternal = await workbenchPage.getTutorialImageByAlt(imageExternalPath); + await workbenchPage.waitUntilImageRendered(imageExternal); + const imageExternalHeight = await imageExternal.getStyleProperty('height'); + await t.expect(parseInt(imageExternalHeight.replace(/[^\d]/g, ''))).gte(150); - // Verify that when User delete the tutorial, then User can see this tutorial and relevant markdown files are deleted from: the Enablement area in Workbench - await t.click(workbenchPage.closeEnablementPage); - await t.click(workbenchPage.tutorialLatestDeleteIcon); - await t.expect(workbenchPage.tutorialDeleteButton.visible).ok('Delete popup is not visible'); - await t.click(workbenchPage.tutorialDeleteButton); - await t.expect(workbenchPage.tutorialDeleteButton.exists).notOk('Delete popup is still visible'); - await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists)) - .notOk(`${tutorialName} tutorial is not uploaded`); -}); + // Verify that user can see image in custom tutorials by providing relative path in md file + const imageRelative = await workbenchPage.getTutorialImageByAlt(imageRelativePath); + await workbenchPage.waitUntilImageRendered(imageRelative); + const imageRelativeHeight = await imageRelative.getStyleProperty('height'); + await t.expect(parseInt(imageRelativeHeight.replace(/[^\d]/g, ''))).gte(150); + + // Verify that when User delete the tutorial, then User can see this tutorial and relevant markdown files are deleted from: the Enablement area in Workbench + await t.click(workbenchPage.closeEnablementPage); + await t.click(workbenchPage.tutorialLatestDeleteIcon); + await t.expect(workbenchPage.tutorialDeleteButton.visible).ok('Delete popup is not visible'); + await t.click(workbenchPage.tutorialDeleteButton); + await t.expect(workbenchPage.tutorialDeleteButton.exists).notOk('Delete popup is still visible'); + await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists)) + .notOk(`${tutorialName} tutorial is not uploaded`); + }); // https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4213, https://redislabs.atlassian.net/browse/RI-4302 test('Verify that user can upload tutorial with URL with manifest.json', async t => { const labelFromManifest = 'LabelFromManifest'; + const link = 'https://drive.google.com/uc?id=1puRUoT8HmyZCekkeWNxBzXe_48TzXcJc&export=download'; internalLinkName1 = 'manifest-id'; + tutorialName = 'tutorialTestByLink'; await t.click(workbenchPage.tutorialOpenUploadButton); // Verify that user can upload tutorials using a URL await t.typeText(workbenchPage.tutorialLinkField, link); await t.click(workbenchPage.tutorialSubmitButton); - await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName2).with({ timeout: 20000 }).visible) - .ok(`${tutorialName2} tutorial is not uploaded`); - await t.click(workbenchPage.tutorialAccordionButton.withText(tutorialName2)); + await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).with({ timeout: 20000 }).visible) + .ok(`${tutorialName} tutorial is not uploaded`); + await t.click(workbenchPage.tutorialAccordionButton.withText(tutorialName)); // Verify that User can see the same structure in the tutorial uploaded as described in the .json manifest await t.expect((await workbenchPage.getInternalLinkWithoutManifest(internalLinkName1)).visible) .ok(`${internalLinkName1} folder specified in manifest is not visible`); @@ -110,6 +132,64 @@ test('Verify that user can upload tutorial with URL with manifest.json', async t await t.click(workbenchPage.tutorialDeleteButton); await t.expect(workbenchPage.tutorialDeleteButton.exists).notOk('Delete popup is still visible'); // Verify that when User delete the tutorial, then User can see this tutorial and relevant markdown files are deleted from: the Enablement area in Workbench - await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName2).exists)) - .notOk(`${tutorialName2} tutorial is not uploaded`); + await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists)) + .notOk(`${tutorialName} tutorial is not uploaded`); }); +test + .before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + tutorialName = `${zipFolderName}${Common.generateWord(5)}`; + zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`); + // Create zip file for uploading + await Common.createZipFromFolder(folderPath, zipFilePath); + }) + .after(async() => { + await Common.deleteFileFromFolder(zipFilePath); + // Clear and delete database + await deleteAllKeysFromDB(ossStandaloneRedisearch.host, ossStandaloneRedisearch.port); + await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + })('Verify that user can bulk upload data from custom tutorial', async t => { + const allKeysResults = ['9Commands Processed', '9Success', '0Errors']; + const absolutePathResults = ['1Commands Processed', '1Success', '0Errors']; + const invalidPathes = ['Invalid relative', 'Invalid absolute']; + const keyNames = ['hashkey1', 'listkey1', 'setkey1', 'zsetkey1', 'stringkey1', 'jsonkey1', 'streamkey1', 'graphkey1', 'tskey1', 'stringkey1test']; + + // Upload custom tutorial + await t + .click(workbenchPage.tutorialOpenUploadButton) + .setFilesToUpload(workbenchPage.tutorialImport, [zipFilePath]) + .click(workbenchPage.tutorialSubmitButton); + await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).visible).ok(`${tutorialName} tutorial is not uploaded`); + // Open tutorial + await t + .click(workbenchPage.tutorialAccordionButton.withText(tutorialName)) + .click(await workbenchPage.getAccordionButtonWithName(folder1)) + .click((await workbenchPage.getInternalLinkWithManifest(internalLinkName1))); + + // Verify that user can bulk upload data by relative path + await t.click((workbenchPage.uploadDataBulkBtn.withExactText('Upload relative'))); + await t.click(workbenchPage.uploadDataBulkApplyBtn); + // Verify that user can see the summary when the command execution is completed + await verifyCompletedResultText(allKeysResults); + + // Verify that user can bulk upload data by absolute path + await t.click((workbenchPage.uploadDataBulkBtn.withExactText('Upload absolute'))); + await t.click(workbenchPage.uploadDataBulkApplyBtn); + await verifyCompletedResultText(absolutePathResults); + + // Verify that user can't upload file by invalid relative path + // Verify that user can't upload file by invalid absolute path + for (const path in invalidPathes) { + await t.click((workbenchPage.uploadDataBulkBtn.withExactText(path))); + await t.click(workbenchPage.uploadDataBulkApplyBtn); + // Verify that user can see standard error messages when any error occurs while finding the file or parsing it + await t.expect(workbenchPage.Toast.toastError.textContent).contains('Data file was not found', 'Bulk upload not failed'); + } + + // Open Browser page + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + // Verify that keys of all types can be uploaded + await browserPage.searchByKeyName('*key1'); + await verifyKeysDisplayedInTheList(keyNames); + }); diff --git a/tests/e2e/tests/smoke/browser/add-keys.e2e.ts b/tests/e2e/tests/smoke/browser/add-keys.e2e.ts index f84c9de1cf..ac141073d2 100644 --- a/tests/e2e/tests/smoke/browser/add-keys.e2e.ts +++ b/tests/e2e/tests/smoke/browser/add-keys.e2e.ts @@ -24,7 +24,7 @@ test('Verify that user can add Hash Key', async t => { // Add Hash key await browserPage.addHashKey(keyName); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The notification not displayed'); // Check that new key is displayed in the list await browserPage.searchByKeyName(keyName); @@ -36,7 +36,7 @@ test('Verify that user can add Set Key', async t => { // Add Set key await browserPage.addSetKey(keyName); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The notification not displayed'); // Check that new key is displayed in the list await browserPage.searchByKeyName(keyName); @@ -48,7 +48,7 @@ test('Verify that user can add List Key', async t => { // Add List key await browserPage.addListKey(keyName); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The notification not displayed'); // Check that new key is displayed in the list await browserPage.searchByKeyName(keyName); @@ -60,7 +60,7 @@ test('Verify that user can add String Key', async t => { // Add String key await browserPage.addStringKey(keyName); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The notification not displayed'); // Check that new key is displayed in the list await browserPage.searchByKeyName(keyName); @@ -72,7 +72,7 @@ test('Verify that user can add ZSet Key', async t => { // Add ZSet key await browserPage.addZSetKey(keyName, '111'); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The notification not displayed'); // Check that new key is displayed in the list await browserPage.searchByKeyName(keyName); @@ -87,7 +87,7 @@ test('Verify that user can add JSON Key', async t => { // Add JSON key await browserPage.addJsonKey(keyName, value, keyTTL); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The notification not displayed'); // Check that new key is displayed in the list await browserPage.searchByKeyName(keyName); diff --git a/tests/e2e/tests/smoke/browser/hash-field.e2e.ts b/tests/e2e/tests/smoke/browser/hash-field.e2e.ts index 1aafd36758..f8b68abbb8 100644 --- a/tests/e2e/tests/smoke/browser/hash-field.e2e.ts +++ b/tests/e2e/tests/smoke/browser/hash-field.e2e.ts @@ -37,6 +37,6 @@ test('Verify that user can add field to Hash', async t => { await t.click(browserPage.removeHashFieldButton); await t.click(browserPage.confirmRemoveHashFieldButton); // Check the notification message - const notofication = await browserPage.getMessageText(); - await t.expect(notofication).contains('Field has been removed', 'The notification is not displayed'); + const notification = browserPage.Toast.toastHeader.textContent; + await t.expect(notification).contains('Field has been removed', 'The notification is not displayed'); }); diff --git a/tests/e2e/tests/smoke/browser/json-key.e2e.ts b/tests/e2e/tests/smoke/browser/json-key.e2e.ts index efd8203ac8..30979b5c10 100644 --- a/tests/e2e/tests/smoke/browser/json-key.e2e.ts +++ b/tests/e2e/tests/smoke/browser/json-key.e2e.ts @@ -27,7 +27,7 @@ test('Verify that user can add key with value to any level of JSON structure', a // Add Json key with json object await browserPage.addJsonKey(keyName, value, keyTTL); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The notification not found'); // Verify that user can create JSON object await t.expect(browserPage.addJsonObjectButton.exists).ok('The add Json object button not found', { timeout: 10000 }); diff --git a/tests/e2e/tests/smoke/browser/list-key.e2e.ts b/tests/e2e/tests/smoke/browser/list-key.e2e.ts index 0330594cfd..8073676724 100644 --- a/tests/e2e/tests/smoke/browser/list-key.e2e.ts +++ b/tests/e2e/tests/smoke/browser/list-key.e2e.ts @@ -36,7 +36,7 @@ test('Verify that user can select remove List element position: from tail', asyn // Remove element from the key await browserPage.removeListElementFromTail('1'); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Elements have been removed', 'The notification not found'); // Check the removed element is not in the list await t.expect(browserPage.listElementsList.withExactText(element3).exists).notOk('The list element not removed', { timeout: 10000 }); @@ -50,7 +50,7 @@ test('Verify that user can select remove List element position: from head', asyn // Remove element from the key await browserPage.removeListElementFromHead('1'); // Check the notification message - const notofication = await browserPage.getMessageText(); + const notofication = browserPage.Toast.toastHeader.textContent; await t.expect(notofication).contains('Elements have been removed', 'The notification not found'); // Check the removed element is not in the list await t.expect(browserPage.listElementsList.withExactText(element).exists).notOk('The list element not removed', { timeout: 10000 }); diff --git a/tests/e2e/tests/smoke/browser/list-of-keys-verifications.e2e.ts b/tests/e2e/tests/smoke/browser/list-of-keys-verifications.e2e.ts index 56b0ed04ac..2a3073368c 100644 --- a/tests/e2e/tests/smoke/browser/list-of-keys-verifications.e2e.ts +++ b/tests/e2e/tests/smoke/browser/list-of-keys-verifications.e2e.ts @@ -54,8 +54,8 @@ test('Verify that user can refresh Keys', async t => { // Add hash key await browserPage.addHashKey(keyName, keyTTL); - const notofication = await browserPage.getMessageText(); - await t.expect(notofication).contains('Key has been added', 'The notification is not displayed'); + const notification = browserPage.Toast.toastHeader.textContent; + await t.expect(notification).contains('Key has been added', 'The notification is not displayed'); await t.click(browserPage.closeKeyButton); // Search for the added key await browserPage.searchByKeyName(keyName); @@ -76,8 +76,8 @@ test('Verify that user can open key details', async t => { // Add String key await browserPage.addStringKey(keyName, keyTTL, keyValue); - const notofication = await browserPage.getMessageText(); - await t.expect(notofication).contains('Key has been added', 'The notification is not displayed'); + const notification = browserPage.Toast.toastHeader.textContent; + await t.expect(notification).contains('Key has been added', 'The notification is not displayed'); await t.click(browserPage.closeKeyButton); // Search for the added key await browserPage.searchByKeyName(keyName); diff --git a/tests/e2e/tests/smoke/browser/set-key.e2e.ts b/tests/e2e/tests/smoke/browser/set-key.e2e.ts index 27da238596..373177e588 100644 --- a/tests/e2e/tests/smoke/browser/set-key.e2e.ts +++ b/tests/e2e/tests/smoke/browser/set-key.e2e.ts @@ -36,6 +36,6 @@ test('Verify that user can remove member from Set', async t => { await t.click(browserPage.removeSetMemberButton); await t.click(browserPage.confirmRemoveSetMemberButton); // Check the notification message - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Member has been removed', 'The notification not found'); }); diff --git a/tests/e2e/tests/smoke/browser/verify-keys-refresh.e2e.ts b/tests/e2e/tests/smoke/browser/verify-keys-refresh.e2e.ts index b49f79d44e..daef813d50 100644 --- a/tests/e2e/tests/smoke/browser/verify-keys-refresh.e2e.ts +++ b/tests/e2e/tests/smoke/browser/verify-keys-refresh.e2e.ts @@ -27,7 +27,7 @@ test('Verify that user can refresh Keys', async t => { // Add hash key await browserPage.addHashKey(keyName, keyTTL); - const notification = await browserPage.getMessageText(); + const notification = browserPage.Toast.toastHeader.textContent; await t.expect(notification).contains('Key has been added', 'The notification not found'); await t.click(browserPage.closeKeyButton); // Search for the added key diff --git a/tests/e2e/tests/smoke/browser/zset-key.e2e.ts b/tests/e2e/tests/smoke/browser/zset-key.e2e.ts index 4d8b2fca79..fa94585a99 100644 --- a/tests/e2e/tests/smoke/browser/zset-key.e2e.ts +++ b/tests/e2e/tests/smoke/browser/zset-key.e2e.ts @@ -35,6 +35,6 @@ test('Verify that user can remove member from ZSet', async t => { await t.click(browserPage.removeZserMemberButton); await t.click(browserPage.confirmRemovZSetMemberButton); // Check the notification message - const notofication = await browserPage.getMessageText(); - await t.expect(notofication).contains('Member has been removed', 'The notification not found'); + const notification = browserPage.Toast.toastHeader.textContent; + await t.expect(notification).contains('Member has been removed', 'The notification not found'); }); diff --git a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts index 77d1911597..209d2b6d9f 100644 --- a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts @@ -64,7 +64,7 @@ test // Wait for database to be exist .expect(myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).exists).ok('The database not displayed', { timeout: 10000 }) // Close message - .click(myRedisDatabasePage.toastCloseButton); + .click(myRedisDatabasePage.Toast.toastCloseButton); // Verify that user can see an indicator of databases that are added manually and not opened yet await myRedisDatabasePage.verifyDatabaseStatusIsVisible(ossStandaloneConfig.databaseName); diff --git a/tests/e2e/yarn.lock b/tests/e2e/yarn.lock index b2f85d7c99..0864e124ba 100644 --- a/tests/e2e/yarn.lock +++ b/tests/e2e/yarn.lock @@ -1210,6 +1210,13 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@types/archiver@^5.3.2": + version "5.3.2" + resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.2.tgz#a9f0bcb0f0b991400e7766d35f6e19d163bdadcc" + integrity sha512-IctHreBuWE5dvBDz/0WeKtyVKVRs4h75IblxOACL92wU66v+HGAfEYAOyXkOFphvRJMhuXdI9huDXpX0FC6lCw== + dependencies: + "@types/readdir-glob" "*" + "@types/chance@1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.1.3.tgz#d19fe9391288d60fdccd87632bfc9ab2b4523fea" @@ -1271,6 +1278,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/readdir-glob@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.1.tgz#27ac2db283e6aa3d110b14ff9da44fcd1a5c38b1" + integrity sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ== + dependencies: + "@types/node" "*" + "@types/set-value@*": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/set-value/-/set-value-4.0.1.tgz#7caf185556a67c2d9051080931853047423c93bd" From f632cc87005104072cd97ee04cfabac90d5c56d4 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 3 May 2023 17:10:06 +0200 Subject: [PATCH 12/32] upd --- tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index 70b062b423..db863238cf 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -135,6 +135,7 @@ test('Verify that user can upload tutorial with URL with manifest.json', async t await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists)) .notOk(`${tutorialName} tutorial is not uploaded`); }); +// https://redislabs.atlassian.net/browse/RI-4352 test .before(async t => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); From 3d69288eb67e10377e0ab8158c74b116bbc6299e Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 3 May 2023 17:19:38 +0200 Subject: [PATCH 13/32] delete console.log --- tests/e2e/helpers/common.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/helpers/common.ts b/tests/e2e/helpers/common.ts index 1d4dc9cde1..1d07d3f846 100644 --- a/tests/e2e/helpers/common.ts +++ b/tests/e2e/helpers/common.ts @@ -200,8 +200,6 @@ export class Common { static async createZipFromFolder(folderPath: string, zipName: string): Promise { const sourceDir = path.join(__dirname, folderPath); const zipFilePath = path.join(__dirname, zipName); - console.log(sourceDir); - console.log(zipFilePath); const output = fs.createWriteStream(zipFilePath); const archive = archiver('zip', { zlib: { level: 9 } }); From 127778bc1326b2ff37055df3334259733c1ff157 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 3 May 2023 17:53:59 +0200 Subject: [PATCH 14/32] fix for archiver --- tests/e2e/helpers/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/helpers/common.ts b/tests/e2e/helpers/common.ts index 1d07d3f846..eadbfdcf7f 100644 --- a/tests/e2e/helpers/common.ts +++ b/tests/e2e/helpers/common.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import * as archiver from 'archiver'; +import archiver from 'archiver'; import * as fs from 'fs'; import { ClientFunction, RequestMock, t } from 'testcafe'; import { Chance } from 'chance'; From 9f3127645585a0d31aaacccd4de627c1f8c8207f Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 3 May 2023 18:45:33 +0200 Subject: [PATCH 15/32] add archiver package --- tests/e2e/helpers/common.ts | 2 +- tests/e2e/package.json | 1 + .../browser/stream-pending-messages.e2e.ts | 2 +- tests/e2e/yarn.lock | 200 +++++++++++++++++- 4 files changed, 200 insertions(+), 5 deletions(-) diff --git a/tests/e2e/helpers/common.ts b/tests/e2e/helpers/common.ts index eadbfdcf7f..1d07d3f846 100644 --- a/tests/e2e/helpers/common.ts +++ b/tests/e2e/helpers/common.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import archiver from 'archiver'; +import * as archiver from 'archiver'; import * as fs from 'fs'; import { ClientFunction, RequestMock, t } from 'testcafe'; import { Chance } from 'chance'; diff --git a/tests/e2e/package.json b/tests/e2e/package.json index a9921047ec..bd81b0619c 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -32,6 +32,7 @@ "@types/supertest": "^2.0.8", "@typescript-eslint/eslint-plugin": "4.28.2", "@typescript-eslint/parser": "4.28.2", + "archiver": "^5.3.1", "chance": "1.1.8", "cross-env": "^7.0.3", "dotenv-cli": "^5.0.0", diff --git a/tests/e2e/tests/regression/browser/stream-pending-messages.e2e.ts b/tests/e2e/tests/regression/browser/stream-pending-messages.e2e.ts index cef02b9d1d..0e6320a3ba 100644 --- a/tests/e2e/tests/regression/browser/stream-pending-messages.e2e.ts +++ b/tests/e2e/tests/regression/browser/stream-pending-messages.e2e.ts @@ -75,7 +75,7 @@ test('Verify that the message is claimed only if its idle time is greater than t await t.click(browserPage.claimPendingMessageButton); await t.typeText(browserPage.streamMinIdleTimeInput, '100000000', { replace: true, paste: true }); await t.click(browserPage.submitButton); - await t.expect(browserPage.notificationMessage.textContent).contains('No messages claimed', 'The message is not claimed notification'); + await t.expect(browserPage.Toast.toastHeader.textContent).contains('No messages claimed', 'The message is not claimed notification'); await t.expect(browserPage.streamMessage.count).eql(streamMessageBefore, 'The number of pendings in the table not correct'); }); test('Verify that when user toggle optional parameters on, he can see optional fields', async t => { diff --git a/tests/e2e/yarn.lock b/tests/e2e/yarn.lock index 0864e124ba..4194755d9e 100644 --- a/tests/e2e/yarn.lock +++ b/tests/e2e/yarn.lock @@ -1498,6 +1498,35 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== +archiver-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" + integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== + dependencies: + glob "^7.1.4" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^2.0.0" + +archiver@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.1.tgz#21e92811d6f09ecfce649fbefefe8c79e57cbbb6" + integrity sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w== + dependencies: + archiver-utils "^2.1.0" + async "^3.2.3" + buffer-crc32 "^0.2.1" + readable-stream "^3.6.0" + readdir-glob "^1.0.0" + tar-stream "^2.2.0" + zip-stream "^4.1.0" + are-we-there-yet@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" @@ -1634,6 +1663,11 @@ async@0.2.6: resolved "https://registry.yarnpkg.com/async/-/async-0.2.6.tgz#ad3f373d9249ae324881565582bc90e152abbd68" integrity sha512-LTdAJ0KBRK5o4BlBlUoGvfGNOMON+NLbONgDZk80SX0G8LQZyjN+74nNADIpQ/+rxun6+fYm7z4vIzAB51UKUA== +async@^3.2.3: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1702,7 +1736,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.1.2: +base64-js@^1.1.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -1725,6 +1759,15 @@ bin-v8-flags-filter@^1.1.2: resolved "https://registry.yarnpkg.com/bin-v8-flags-filter/-/bin-v8-flags-filter-1.2.0.tgz#023fc4ee22999b2b1f6dd1b7253621366841537e" integrity sha512-g8aeYkY7GhyyKRvQMBsJQZjhm2iCX3dKYvfrMpwVR8IxmUGrkpCBFoKbB9Rh0o3sTLCjU/1tFpZ4C7j3f+D+3g== +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird@^3.5.0: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -1748,6 +1791,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -1788,11 +1838,24 @@ browserslist@^4.21.3, browserslist@^4.21.4: node-releases "^2.0.6" update-browserslist-db "^1.0.9" +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + cacache@^15.2.0: version "15.3.0" resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" @@ -2030,6 +2093,16 @@ component-emitter@^1.2.0, component-emitter@^1.2.1: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +compress-commons@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" + integrity sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ== + dependencies: + buffer-crc32 "^0.2.13" + crc32-stream "^4.0.2" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2072,6 +2145,19 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +crc-32@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +crc32-stream@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.2.tgz#c922ad22b38395abe9d3870f02fa8134ed709007" + integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w== + dependencies: + crc-32 "^1.2.0" + readable-stream "^3.4.0" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -2362,7 +2448,7 @@ encoding@^0.1.12: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -2897,6 +2983,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -3121,6 +3212,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.2.2, graceful-fs@^4.2.6: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graphlib@^2.1.5: version "2.1.8" resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" @@ -3277,6 +3373,11 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^4.0.3, ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -3332,7 +3433,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3795,6 +3896,13 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -3841,6 +3949,26 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA== + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -3851,6 +3979,11 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== +lodash.union@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== + "lodash@4.6.1 || ^4.16.1", lodash@^4.14.0, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -4038,6 +4171,13 @@ minimatch@^3.0.4, minimatch@^3.1.1: dependencies: brace-expansion "^1.1.7" +minimatch@^5.1.0: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" @@ -4265,6 +4405,11 @@ normalize-package-data@^2.3.2: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + npm-run-path@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -4722,6 +4867,19 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" +readable-stream@^2.0.0, readable-stream@^2.0.5: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@^2.0.1, readable-stream@^2.3.5: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -4735,6 +4893,15 @@ readable-stream@^2.0.1, readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -4744,6 +4911,13 @@ readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdir-glob@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + redis-commands@^1.5.0: version "1.7.0" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" @@ -5369,6 +5543,17 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" +tar-stream@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: version "6.1.13" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" @@ -6074,3 +6259,12 @@ yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +zip-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" + integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== + dependencies: + archiver-utils "^2.1.0" + compress-commons "^4.1.0" + readable-stream "^3.6.0" From 00a3edc299e431ac43dfebcb14984b8080f51ca6 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 3 May 2023 21:34:35 +0200 Subject: [PATCH 16/32] fixes by pr comments --- tests/e2e/helpers/common.ts | 8 ++------ tests/e2e/pageObjects/workbench-page.ts | 11 +++++++++++ .../regression/workbench/import-tutorials.e2e.ts | 7 ++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/e2e/helpers/common.ts b/tests/e2e/helpers/common.ts index 1d07d3f846..4a3bc52228 100644 --- a/tests/e2e/helpers/common.ts +++ b/tests/e2e/helpers/common.ts @@ -205,13 +205,9 @@ export class Common { // Add the contents of the directory to the zip archive archive.directory(sourceDir, false); - // Finalize the archive and write it to disk - archive.finalize(); - await new Promise((resolve) => { - output.on('close', resolve); - archive.pipe(output); - }); + await archive.finalize(); + archive.pipe(output); } /** diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index 5d430a893e..7690380f2f 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -25,6 +25,7 @@ export class WorkbenchPage extends InstancePage { cssCustomPluginTableResult = '[data-testid^=query-table-result-client]'; cssCommandExecutionDateTime = '[data-testid=command-execution-date-time]'; cssRowInVirtualizedTable = '[data-testid^=row-]'; + cssTutorialDeleteIcon = '[data-testid^=delete-tutorial-icon-]'; //------------------------------------------------------------------------------------------- //DECLARATION OF SELECTORS //*Declare all elements/components of the relevant page. @@ -278,6 +279,16 @@ export class WorkbenchPage extends InstancePage { return Selector('div').withText(name); } + /** + * Delete tutorial by name + * @param name A tutorial name + */ + async deleteTutorialByName(name: string): Promise { + const deleteTutorialBtn = this.tutorialAccordionButton.withText(name).find(this.cssTutorialDeleteIcon); + await t.click(deleteTutorialBtn); + await t.click(this.tutorialDeleteButton); + } + /** * Find image in tutorial by alt text * @param alt Image alt text diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index db863238cf..7f6eed477b 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -147,14 +147,19 @@ test }) .after(async() => { await Common.deleteFileFromFolder(zipFilePath); - // Clear and delete database await deleteAllKeysFromDB(ossStandaloneRedisearch.host, ossStandaloneRedisearch.port); + // Clear and delete database + await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await workbenchPage.deleteTutorialByName(tutorialName); + await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists)) + .notOk(`${tutorialName} tutorial is not deleted`); await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Verify that user can bulk upload data from custom tutorial', async t => { const allKeysResults = ['9Commands Processed', '9Success', '0Errors']; const absolutePathResults = ['1Commands Processed', '1Success', '0Errors']; const invalidPathes = ['Invalid relative', 'Invalid absolute']; const keyNames = ['hashkey1', 'listkey1', 'setkey1', 'zsetkey1', 'stringkey1', 'jsonkey1', 'streamkey1', 'graphkey1', 'tskey1', 'stringkey1test']; + internalLinkName1 = 'probably-1'; // Upload custom tutorial await t From 37bcc1735563a1bb857ee63cb748519f38601a1b Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 3 May 2023 23:27:59 +0200 Subject: [PATCH 17/32] test of CI --- .circleci/config.yml | 6 +++--- .../customTutorials/folder-1/probably-1.md | 2 +- .../regression/workbench/import-tutorials.e2e.ts | 13 +++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ecb4bbfd9c..8ef7a0952b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -308,7 +308,7 @@ jobs: description: Number of threads to run tests type: integer default: 1 - parallelism: << parameters.parallelism >> + parallelism: 1 steps: - checkout - attach_workspace: @@ -389,7 +389,7 @@ jobs: description: Number of threads to run tests type: integer default: 1 - parallelism: << parameters.parallelism >> + parallelism: 1 steps: - checkout - when: @@ -1086,7 +1086,7 @@ workflows: - e2e-tests: name: E2ETest build: docker - parallelism: 4 + parallelism: 1 requires: - Build docker image diff --git a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md index 378d612d36..27f9133241 100644 --- a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md +++ b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md @@ -2,7 +2,7 @@ In very broad terms probabilistic data structures (PDS) allow us to get to a "cl External: -![RedisInsight screen external](https://github.com/RedisInsight/RedisInsight/blob/main/.github/redisinsight_browser.png?raw=true) + Relative: diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index 7f6eed477b..01ac5c0c37 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -28,7 +28,7 @@ const verifyCompletedResultText = async(resultsText: string[]): Promise => await t.expect(workbenchPage.Toast.toastBody.textContent).notContains('0:00:00.00', 'Bulk upload Time taken not correct'); }; -fixture `Upload custom tutorials` +fixture.only `Upload custom tutorials` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { @@ -57,7 +57,7 @@ test })('Verify that user can upload tutorial with local zip file without manifest.json', async t => { // Verify that user can upload custom tutorials on docker version internalLinkName1 = 'probably-1'; - const imageExternalPath = 'RedisInsight screen external'; + // const imageExternalPath = 'RedisInsight screen external'; const imageRelativePath = 'RedisInsight screen relative'; // Verify that user can see the “MY TUTORIALS” section in the Enablement area. @@ -84,11 +84,12 @@ test await t.click((await workbenchPage.getInternalLinkWithManifest(internalLinkName1))); await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked'); + // Uncomment after fix for CI - error for tutorial with image // Verify that user can see image in custom tutorials by providing absolute external path in md file - const imageExternal = await workbenchPage.getTutorialImageByAlt(imageExternalPath); - await workbenchPage.waitUntilImageRendered(imageExternal); - const imageExternalHeight = await imageExternal.getStyleProperty('height'); - await t.expect(parseInt(imageExternalHeight.replace(/[^\d]/g, ''))).gte(150); + // const imageExternal = await workbenchPage.getTutorialImageByAlt(imageExternalPath); + // await workbenchPage.waitUntilImageRendered(imageExternal); + // const imageExternalHeight = await imageExternal.getStyleProperty('height'); + // await t.expect(parseInt(imageExternalHeight.replace(/[^\d]/g, ''))).gte(150); // Verify that user can see image in custom tutorials by providing relative path in md file const imageRelative = await workbenchPage.getTutorialImageByAlt(imageRelativePath); From f2f5224a096243ebfbea8de4ccd0d9bc544c77b0 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 3 May 2023 23:52:01 +0200 Subject: [PATCH 18/32] test of CI 2 --- .../customTutorials/folder-1/probably-1.md | 4 ++-- .../workbench/import-tutorials.e2e.ts | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md index 27f9133241..c056a5381c 100644 --- a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md +++ b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md @@ -2,11 +2,11 @@ In very broad terms probabilistic data structures (PDS) allow us to get to a "cl External: - +![RedisInsight screen external](https://github.com/RedisInsight/RedisInsight/blob/main/.github/redisinsight_browser.png?raw=true) Relative: -![RedisInsight screen relative](../_images/image.png) + Relative path button: diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index 01ac5c0c37..135e91eddd 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -57,8 +57,8 @@ test })('Verify that user can upload tutorial with local zip file without manifest.json', async t => { // Verify that user can upload custom tutorials on docker version internalLinkName1 = 'probably-1'; - // const imageExternalPath = 'RedisInsight screen external'; - const imageRelativePath = 'RedisInsight screen relative'; + const imageExternalPath = 'RedisInsight screen external'; + // const imageRelativePath = 'RedisInsight screen relative'; // Verify that user can see the “MY TUTORIALS” section in the Enablement area. await t.expect(workbenchPage.customTutorials.visible).ok('custom tutorials sections is not visible'); @@ -84,18 +84,18 @@ test await t.click((await workbenchPage.getInternalLinkWithManifest(internalLinkName1))); await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked'); - // Uncomment after fix for CI - error for tutorial with image // Verify that user can see image in custom tutorials by providing absolute external path in md file - // const imageExternal = await workbenchPage.getTutorialImageByAlt(imageExternalPath); - // await workbenchPage.waitUntilImageRendered(imageExternal); - // const imageExternalHeight = await imageExternal.getStyleProperty('height'); - // await t.expect(parseInt(imageExternalHeight.replace(/[^\d]/g, ''))).gte(150); + const imageExternal = await workbenchPage.getTutorialImageByAlt(imageExternalPath); + await workbenchPage.waitUntilImageRendered(imageExternal); + const imageExternalHeight = await imageExternal.getStyleProperty('height'); + await t.expect(parseInt(imageExternalHeight.replace(/[^\d]/g, ''))).gte(150); + // Uncomment after fix for CI - error for tutorial with image // Verify that user can see image in custom tutorials by providing relative path in md file - const imageRelative = await workbenchPage.getTutorialImageByAlt(imageRelativePath); - await workbenchPage.waitUntilImageRendered(imageRelative); - const imageRelativeHeight = await imageRelative.getStyleProperty('height'); - await t.expect(parseInt(imageRelativeHeight.replace(/[^\d]/g, ''))).gte(150); + // const imageRelative = await workbenchPage.getTutorialImageByAlt(imageRelativePath); + // await workbenchPage.waitUntilImageRendered(imageRelative); + // const imageRelativeHeight = await imageRelative.getStyleProperty('height'); + // await t.expect(parseInt(imageRelativeHeight.replace(/[^\d]/g, ''))).gte(150); // Verify that when User delete the tutorial, then User can see this tutorial and relevant markdown files are deleted from: the Enablement area in Workbench await t.click(workbenchPage.closeEnablementPage); From fa89a8de69768618febc0502d1e6fd9653149c37 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 00:33:35 +0200 Subject: [PATCH 19/32] update because of found bug --- .../upload-tutorials/customTutorials/folder-1/probably-1.md | 4 ---- tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md index c056a5381c..9019e5f26a 100644 --- a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md +++ b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md @@ -4,10 +4,6 @@ External: ![RedisInsight screen external](https://github.com/RedisInsight/RedisInsight/blob/main/.github/redisinsight_browser.png?raw=true) -Relative: - - - Relative path button: ```redis-upload:[../../_upload/bulkUplAllKeyTypes.txt] Upload relative diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index 135e91eddd..b782c5873b 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -90,7 +90,10 @@ test const imageExternalHeight = await imageExternal.getStyleProperty('height'); await t.expect(parseInt(imageExternalHeight.replace(/[^\d]/g, ''))).gte(150); - // Uncomment after fix for CI - error for tutorial with image + /* Uncomment after fix https://redislabs.atlassian.net/browse/RI-4486 + also need to add in probably-1.md file: + Relative: + ![RedisInsight screen relative](../_images/image.png) */ // Verify that user can see image in custom tutorials by providing relative path in md file // const imageRelative = await workbenchPage.getTutorialImageByAlt(imageRelativePath); // await workbenchPage.waitUntilImageRendered(imageRelative); From b453b46fd97697f99efdb28715b615bfa6e3071a Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 00:35:20 +0200 Subject: [PATCH 20/32] return parallelizm --- .circleci/config.yml | 6 +++--- .../e2e/tests/regression/workbench/import-tutorials.e2e.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8ef7a0952b..ecb4bbfd9c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -308,7 +308,7 @@ jobs: description: Number of threads to run tests type: integer default: 1 - parallelism: 1 + parallelism: << parameters.parallelism >> steps: - checkout - attach_workspace: @@ -389,7 +389,7 @@ jobs: description: Number of threads to run tests type: integer default: 1 - parallelism: 1 + parallelism: << parameters.parallelism >> steps: - checkout - when: @@ -1086,7 +1086,7 @@ workflows: - e2e-tests: name: E2ETest build: docker - parallelism: 1 + parallelism: 4 requires: - Build docker image diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index b782c5873b..935f4b9c26 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -28,7 +28,7 @@ const verifyCompletedResultText = async(resultsText: string[]): Promise => await t.expect(workbenchPage.Toast.toastBody.textContent).notContains('0:00:00.00', 'Bulk upload Time taken not correct'); }; -fixture.only `Upload custom tutorials` +fixture `Upload custom tutorials` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { From 80a55edc3b5a78646276e8dd6ce8824b3ffef42f Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 00:54:54 +0200 Subject: [PATCH 21/32] fix --- tests/e2e/pageObjects/workbench-page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index 7690380f2f..162252981f 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -38,7 +38,7 @@ export class WorkbenchPage extends InstancePage { tutorialOpenUploadButton = Selector('[data-testid=open-upload-tutorial-btn]'); tutorialLinkField = Selector('[data-testid=tutorial-link-field]'); tutorialLatestDeleteIcon = Selector('[data-testid^=delete-tutorial-icon-]').nth(0); - tutorialDeleteButton = Selector('[data-testid^=delete-tutorial-]').withText('Delete'); + tutorialDeleteButton = Selector('button[data-testid^=delete-tutorial-]'); tutorialNameField = Selector('[data-testid=tutorial-name-field]'); tutorialSubmitButton = Selector('[data-testid=submit-upload-tutorial-btn]'); tutorialImport = Selector('[data-testid=import-tutorial]'); From 6bed8527b5f51f13cb15d2b542fef718a429de35 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 01:36:07 +0200 Subject: [PATCH 22/32] fixes --- tests/e2e/pageObjects/browser-page.ts | 1 - tests/e2e/pageObjects/components/toast.ts | 2 ++ tests/e2e/pageObjects/workbench-page.ts | 3 +++ .../upload-tutorials/customTutorials/folder-1/probably-1.md | 4 ++-- .../memory-efficiency/memory-efficiency.e2e.ts | 4 ++-- .../e2e/tests/regression/workbench/import-tutorials.e2e.ts | 6 ++++-- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index cea7a13c09..fd61c45994 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -102,7 +102,6 @@ export class BrowserPage extends InstancePage { editZsetButton = Selector('[data-testid^=zset-edit-button-]'); editListButton = Selector('[data-testid^=edit-list-button-]'); cancelStreamGroupBtn = Selector('[data-testid=cancel-stream-groups-btn]'); - submitTooltipBtn = Selector('[data-testid=submit-tooltip-btn]'); patternModeBtn = Selector('[data-testid=search-mode-pattern-btn]'); redisearchModeBtn = Selector('[data-testid=search-mode-redisearch-btn]'); showFilterHistoryBtn = Selector('[data-testid=show-suggestions-btn]'); diff --git a/tests/e2e/pageObjects/components/toast.ts b/tests/e2e/pageObjects/components/toast.ts index fe5cc0c9bc..a3afa68aa7 100644 --- a/tests/e2e/pageObjects/components/toast.ts +++ b/tests/e2e/pageObjects/components/toast.ts @@ -6,4 +6,6 @@ export class Toast { toastSuccess = Selector('[class*=euiToast--success]'); toastError = Selector('[class*=euiToast--danger]'); toastCloseButton = Selector('[data-test-subj=toastCloseButton]'); + toastSubmitBtn = Selector('[data-testid=submit-tooltip-btn]'); + toastCancelBtn = Selector('[data-testid=toast-cancel-btn]'); } diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index 162252981f..38b96ae909 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -285,6 +285,9 @@ export class WorkbenchPage extends InstancePage { */ async deleteTutorialByName(name: string): Promise { const deleteTutorialBtn = this.tutorialAccordionButton.withText(name).find(this.cssTutorialDeleteIcon); + if (await this.closeEnablementPage.exists) { + await t.click(this.closeEnablementPage); + } await t.click(deleteTutorialBtn); await t.click(this.tutorialDeleteButton); } diff --git a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md index 9019e5f26a..f56b70e030 100644 --- a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md +++ b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md @@ -6,7 +6,7 @@ External: Relative path button: -```redis-upload:[../../_upload/bulkUplAllKeyTypes.txt] Upload relative +```redis-upload:[../_upload/bulkUplAllKeyTypes.txt] Upload relative ``` Relative path long name button: @@ -26,7 +26,7 @@ Invalid absolute path button: Invalid relative path button: -```redis-upload:[../_upload/bulkUplAllKeyTypes.txt] Invalid relative +```redis-upload:[../../_upload/bulkUplAllKeyTypes.txt] Invalid relative ``` Redis Stack supports 4 of the most famous PDS: diff --git a/tests/e2e/tests/critical-path/memory-efficiency/memory-efficiency.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/memory-efficiency.e2e.ts index 91524abf05..5179407fa3 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/memory-efficiency.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/memory-efficiency.e2e.ts @@ -57,8 +57,8 @@ test await browserPage.addHashKey(hashKeyName, keysTTL[2], hashValue); await browserPage.addStreamKey(streamKeyName, 'field', 'value', keysTTL[2]); await browserPage.addStreamKey(streamKeyNameDelimiter, 'field', 'value', keysTTL[2]); - if (await browserPage.submitTooltipBtn.exists) { - await t.click(browserPage.submitTooltipBtn); + if (await browserPage.Toast.toastSubmitBtn.exists) { + await t.click(browserPage.Toast.toastSubmitBtn); } await browserPage.Cli.addKeysFromCliWithDelimiter('MSET', 15); await t.click(browserPage.treeViewButton); diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index 935f4b9c26..a6da87149e 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -26,6 +26,7 @@ const verifyCompletedResultText = async(resultsText: string[]): Promise => await t.expect(workbenchPage.Toast.toastBody.textContent).contains(result, 'Bulk upload completed summary not correct'); } await t.expect(workbenchPage.Toast.toastBody.textContent).notContains('0:00:00.00', 'Bulk upload Time taken not correct'); + await t.click(workbenchPage.Toast.toastSubmitBtn); }; fixture `Upload custom tutorials` @@ -190,16 +191,17 @@ test // Verify that user can't upload file by invalid relative path // Verify that user can't upload file by invalid absolute path - for (const path in invalidPathes) { + for (const path of invalidPathes) { await t.click((workbenchPage.uploadDataBulkBtn.withExactText(path))); await t.click(workbenchPage.uploadDataBulkApplyBtn); // Verify that user can see standard error messages when any error occurs while finding the file or parsing it await t.expect(workbenchPage.Toast.toastError.textContent).contains('Data file was not found', 'Bulk upload not failed'); + await t.click(workbenchPage.Toast.toastCancelBtn); } // Open Browser page await t.click(myRedisDatabasePage.NavigationPanel.browserButton); // Verify that keys of all types can be uploaded - await browserPage.searchByKeyName('*key1'); + await browserPage.searchByKeyName('*key1*'); await verifyKeysDisplayedInTheList(keyNames); }); From 160e6af0e610398682156fa30d8bd9f3232ecab5 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 4 May 2023 07:50:47 +0300 Subject: [PATCH 23/32] additionally normalize path (quick fix) --- redisinsight/ui/src/utils/pathUtil.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/utils/pathUtil.ts b/redisinsight/ui/src/utils/pathUtil.ts index ce2ee35671..3fc6a7e4ae 100644 --- a/redisinsight/ui/src/utils/pathUtil.ts +++ b/redisinsight/ui/src/utils/pathUtil.ts @@ -15,7 +15,10 @@ export const prepareTutorialDataFileUrlFromMd = (nodeUrl: string, mdPath: string // process absolute path if (nodeUrl.startsWith('/') || nodeUrl.startsWith('\\')) { - const paths = mdPath?.split('/') || [] + // todo: quick fix. find the root cause why path has both '/' and '\' + const normalizedMdPath = mdPath.replaceAll('\\', '/') + + const paths = normalizedMdPath?.split('/') || [] let tutorialRootPath switch (paths[1]) { case TutorialsPaths.CustomTutorials: @@ -26,7 +29,7 @@ export const prepareTutorialDataFileUrlFromMd = (nodeUrl: string, mdPath: string tutorialRootPath = paths.slice(0, 2).join('/') break default: - tutorialRootPath = mdPath + tutorialRootPath = normalizedMdPath break } From 2c948a99c465c58ef8c24f4de8b4823862c033a6 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 10:44:04 +0200 Subject: [PATCH 24/32] update --- .../customTutorials/folder-1/probably-1.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md index f56b70e030..d392e2d292 100644 --- a/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md +++ b/tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md @@ -1,9 +1,5 @@ In very broad terms probabilistic data structures (PDS) allow us to get to a "close enough" result in a much shorter time and by using significantly less memory. -External: - -![RedisInsight screen external](https://github.com/RedisInsight/RedisInsight/blob/main/.github/redisinsight_browser.png?raw=true) - Relative path button: ```redis-upload:[../_upload/bulkUplAllKeyTypes.txt] Upload relative @@ -19,6 +15,10 @@ Absolute path button: ```redis-upload:[/_upload/bulkUplString.txt] Upload absolute ``` +External: + +![RedisInsight screen external](https://github.com/RedisInsight/RedisInsight/blob/main/.github/redisinsight_browser.png?raw=true) + Invalid absolute path button: ```redis-upload:[/_upload/bulkUplAllKeyTypes] Invalid absolute From 85a93d9aabc3a8425654c7d3079a827b57903ace Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 11:39:09 +0200 Subject: [PATCH 25/32] fix for CI --- tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index a6da87149e..c644bb25b3 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -177,6 +177,7 @@ test .click(workbenchPage.tutorialAccordionButton.withText(tutorialName)) .click(await workbenchPage.getAccordionButtonWithName(folder1)) .click((await workbenchPage.getInternalLinkWithManifest(internalLinkName1))); + await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('Enablement area is not visible after clicked'); // Verify that user can bulk upload data by relative path await t.click((workbenchPage.uploadDataBulkBtn.withExactText('Upload relative'))); From 2eb37c5be5af71ecde798ca82f8413e69d67855f Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 12:48:56 +0200 Subject: [PATCH 26/32] upd for CI --- .circleci/config.yml | 4 +- tests/e2e/pageObjects/workbench-page.ts | 10 ++--- .../workbench/import-tutorials.e2e.ts | 40 +++++++++---------- tests/e2e/web.runner.ts | 4 +- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ecb4bbfd9c..c8552f0baa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -389,7 +389,7 @@ jobs: description: Number of threads to run tests type: integer default: 1 - parallelism: << parameters.parallelism >> + parallelism: 1 steps: - checkout - when: @@ -1086,7 +1086,7 @@ workflows: - e2e-tests: name: E2ETest build: docker - parallelism: 4 + parallelism: 1 requires: - Build docker image diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index 38b96ae909..74f8b90899 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -251,7 +251,7 @@ export class WorkbenchPage extends InstancePage { * Get selector with tutorial name * @param tutorialName name of the uploaded tutorial */ - async getAccordionButtonWithName(tutorialName: string): Promise { + getAccordionButtonWithName(tutorialName: string): Selector { return Selector(`[data-testid=accordion-button-${tutorialName}]`); } @@ -259,7 +259,7 @@ export class WorkbenchPage extends InstancePage { * Get internal tutorial link with .md name * @param internalLink name of the .md file */ - async getInternalLinkWithManifest(internalLink: string): Promise { + getInternalLinkWithManifest(internalLink: string): Selector { return Selector(`[data-testid="internal-link-${internalLink}.md"]`); } @@ -267,7 +267,7 @@ export class WorkbenchPage extends InstancePage { * Get internal tutorial link without .md name * @param internalLink name of the label */ - async getInternalLinkWithoutManifest(internalLink: string): Promise { + getInternalLinkWithoutManifest(internalLink: string): Selector { return Selector(`[data-testid="internal-link-${internalLink}"]`); } @@ -275,7 +275,7 @@ export class WorkbenchPage extends InstancePage { * Find tutorial selector by name * @param name A tutorial name */ - async getTutorialByName(name: string): Promise { + getTutorialByName(name: string): Selector { return Selector('div').withText(name); } @@ -296,7 +296,7 @@ export class WorkbenchPage extends InstancePage { * Find image in tutorial by alt text * @param alt Image alt text */ - async getTutorialImageByAlt(alt: string): Promise { + getTutorialImageByAlt(alt: string): Selector { return Selector('img').withAttribute('alt', alt); } diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index c644bb25b3..4089bda092 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -29,7 +29,7 @@ const verifyCompletedResultText = async(resultsText: string[]): Promise => await t.click(workbenchPage.Toast.toastSubmitBtn); }; -fixture `Upload custom tutorials` +fixture.only `Upload custom tutorials` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { @@ -73,20 +73,20 @@ test // Verify that when user upload a .zip archive without a .json manifest, all markdown files are inserted at the same hierarchy level await t.click(workbenchPage.tutorialAccordionButton.withText(tutorialName)); - await t.expect((await workbenchPage.getAccordionButtonWithName(folder1)).visible).ok(`${folder1} is not visible`); - await t.expect((await workbenchPage.getAccordionButtonWithName(folder2)).visible).ok(`${folder2} is not visible`); - await t.click(await workbenchPage.getAccordionButtonWithName(folder1)); - await t.expect((await workbenchPage.getInternalLinkWithManifest(internalLinkName1)).visible) + await t.expect(workbenchPage.getAccordionButtonWithName(folder1).visible).ok(`${folder1} is not visible`); + await t.expect(workbenchPage.getAccordionButtonWithName(folder2).visible).ok(`${folder2} is not visible`); + await t.click(workbenchPage.getAccordionButtonWithName(folder1)); + await t.expect(workbenchPage.getInternalLinkWithManifest(internalLinkName1).visible) .ok(`${internalLinkName1} is not visible`); - await t.click(await workbenchPage.getAccordionButtonWithName(folder2)); - await t.expect((await workbenchPage.getInternalLinkWithManifest(internalLinkName2)).visible) + await t.click(workbenchPage.getAccordionButtonWithName(folder2)); + await t.expect(workbenchPage.getInternalLinkWithManifest(internalLinkName2).visible) .ok(`${internalLinkName2} is not visible`); await t.expect(workbenchPage.scrolledEnablementArea.exists).notOk('enablement area is visible before clicked'); - await t.click((await workbenchPage.getInternalLinkWithManifest(internalLinkName1))); + await t.click(workbenchPage.getInternalLinkWithManifest(internalLinkName1)); await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked'); // Verify that user can see image in custom tutorials by providing absolute external path in md file - const imageExternal = await workbenchPage.getTutorialImageByAlt(imageExternalPath); + const imageExternal = workbenchPage.getTutorialImageByAlt(imageExternalPath); await workbenchPage.waitUntilImageRendered(imageExternal); const imageExternalHeight = await imageExternal.getStyleProperty('height'); await t.expect(parseInt(imageExternalHeight.replace(/[^\d]/g, ''))).gte(150); @@ -107,7 +107,7 @@ test await t.expect(workbenchPage.tutorialDeleteButton.visible).ok('Delete popup is not visible'); await t.click(workbenchPage.tutorialDeleteButton); await t.expect(workbenchPage.tutorialDeleteButton.exists).notOk('Delete popup is still visible'); - await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists)) + await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).exists) .notOk(`${tutorialName} tutorial is not uploaded`); }); // https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4213, https://redislabs.atlassian.net/browse/RI-4302 @@ -125,11 +125,11 @@ test('Verify that user can upload tutorial with URL with manifest.json', async t .ok(`${tutorialName} tutorial is not uploaded`); await t.click(workbenchPage.tutorialAccordionButton.withText(tutorialName)); // Verify that User can see the same structure in the tutorial uploaded as described in the .json manifest - await t.expect((await workbenchPage.getInternalLinkWithoutManifest(internalLinkName1)).visible) + await t.expect(workbenchPage.getInternalLinkWithoutManifest(internalLinkName1).visible) .ok(`${internalLinkName1} folder specified in manifest is not visible`); - await t.expect(await (await workbenchPage.getInternalLinkWithoutManifest(internalLinkName1)).textContent) + await t.expect(workbenchPage.getInternalLinkWithoutManifest(internalLinkName1).textContent) .eql(labelFromManifest, `${labelFromManifest} tutorial specified in manifest is not visible`); - await t.click((await workbenchPage.getInternalLinkWithoutManifest(internalLinkName1))); + await t.click(workbenchPage.getInternalLinkWithoutManifest(internalLinkName1)); await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked'); await t.click(workbenchPage.closeEnablementPage); await t.click(workbenchPage.tutorialLatestDeleteIcon); @@ -137,7 +137,7 @@ test('Verify that user can upload tutorial with URL with manifest.json', async t await t.click(workbenchPage.tutorialDeleteButton); await t.expect(workbenchPage.tutorialDeleteButton.exists).notOk('Delete popup is still visible'); // Verify that when User delete the tutorial, then User can see this tutorial and relevant markdown files are deleted from: the Enablement area in Workbench - await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists)) + await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).exists) .notOk(`${tutorialName} tutorial is not uploaded`); }); // https://redislabs.atlassian.net/browse/RI-4352 @@ -156,7 +156,7 @@ test // Clear and delete database await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); await workbenchPage.deleteTutorialByName(tutorialName); - await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists)) + await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).exists) .notOk(`${tutorialName} tutorial is not deleted`); await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Verify that user can bulk upload data from custom tutorial', async t => { @@ -175,25 +175,25 @@ test // Open tutorial await t .click(workbenchPage.tutorialAccordionButton.withText(tutorialName)) - .click(await workbenchPage.getAccordionButtonWithName(folder1)) - .click((await workbenchPage.getInternalLinkWithManifest(internalLinkName1))); + .click(workbenchPage.getAccordionButtonWithName(folder1)) + .click(workbenchPage.getInternalLinkWithManifest(internalLinkName1)); await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('Enablement area is not visible after clicked'); // Verify that user can bulk upload data by relative path - await t.click((workbenchPage.uploadDataBulkBtn.withExactText('Upload relative'))); + await t.click(workbenchPage.uploadDataBulkBtn.withExactText('Upload relative')); await t.click(workbenchPage.uploadDataBulkApplyBtn); // Verify that user can see the summary when the command execution is completed await verifyCompletedResultText(allKeysResults); // Verify that user can bulk upload data by absolute path - await t.click((workbenchPage.uploadDataBulkBtn.withExactText('Upload absolute'))); + await t.click(workbenchPage.uploadDataBulkBtn.withExactText('Upload absolute')); await t.click(workbenchPage.uploadDataBulkApplyBtn); await verifyCompletedResultText(absolutePathResults); // Verify that user can't upload file by invalid relative path // Verify that user can't upload file by invalid absolute path for (const path of invalidPathes) { - await t.click((workbenchPage.uploadDataBulkBtn.withExactText(path))); + await t.click(workbenchPage.uploadDataBulkBtn.withExactText(path)); await t.click(workbenchPage.uploadDataBulkApplyBtn); // Verify that user can see standard error messages when any error occurs while finding the file or parsing it await t.expect(workbenchPage.Toast.toastError.textContent).contains('Data file was not found', 'Bulk upload not failed'); diff --git a/tests/e2e/web.runner.ts b/tests/e2e/web.runner.ts index 912602f90a..667cfd82d2 100644 --- a/tests/e2e/web.runner.ts +++ b/tests/e2e/web.runner.ts @@ -6,7 +6,7 @@ import testcafe from 'testcafe'; return t .createRunner() .src((process.env.TEST_FILES || 'tests/**/*.e2e.ts').split('\n')) - .browsers(['chromium:headless --cache --allow-insecure-localhost --ignore-certificate-errors']) + .browsers(['chromium:headless -q attemptLimit=1 --cache --allow-insecure-localhost --ignore-certificate-errors']) .filter((_testName, _fixtureName, _fixturePath, testMeta): boolean => { return testMeta.env !== 'desktop' }) @@ -36,7 +36,7 @@ import testcafe from 'testcafe'; selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, - quarantineMode: { successThreshold: '1', attemptLimit: '3' } + quarantineMode: { successThreshold: '1', attemptLimit: '1' } }); }) .then((failedCount) => { From e9e7dbbcf3495c1cd7f48396b2528b109896258b Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 13:18:29 +0200 Subject: [PATCH 27/32] delete attempt limit --- tests/e2e/web.runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/web.runner.ts b/tests/e2e/web.runner.ts index 667cfd82d2..7c067a9f0e 100644 --- a/tests/e2e/web.runner.ts +++ b/tests/e2e/web.runner.ts @@ -6,7 +6,7 @@ import testcafe from 'testcafe'; return t .createRunner() .src((process.env.TEST_FILES || 'tests/**/*.e2e.ts').split('\n')) - .browsers(['chromium:headless -q attemptLimit=1 --cache --allow-insecure-localhost --ignore-certificate-errors']) + .browsers(['chromium:headless --cache --allow-insecure-localhost --ignore-certificate-errors']) .filter((_testName, _fixtureName, _fixturePath, testMeta): boolean => { return testMeta.env !== 'desktop' }) From 8944710fb01b76e192f0a05d967c194540097c44 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 13:39:33 +0200 Subject: [PATCH 28/32] fix --- .../e2e/tests/regression/workbench/import-tutorials.e2e.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index 4089bda092..ebdb9f256d 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -180,20 +180,20 @@ test await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('Enablement area is not visible after clicked'); // Verify that user can bulk upload data by relative path - await t.click(workbenchPage.uploadDataBulkBtn.withExactText('Upload relative')); + await t.click(workbenchPage.uploadDataBulkBtn.withText('Upload relative')); await t.click(workbenchPage.uploadDataBulkApplyBtn); // Verify that user can see the summary when the command execution is completed await verifyCompletedResultText(allKeysResults); // Verify that user can bulk upload data by absolute path - await t.click(workbenchPage.uploadDataBulkBtn.withExactText('Upload absolute')); + await t.click(workbenchPage.uploadDataBulkBtn.withText('Upload absolute')); await t.click(workbenchPage.uploadDataBulkApplyBtn); await verifyCompletedResultText(absolutePathResults); // Verify that user can't upload file by invalid relative path // Verify that user can't upload file by invalid absolute path for (const path of invalidPathes) { - await t.click(workbenchPage.uploadDataBulkBtn.withExactText(path)); + await t.click(workbenchPage.uploadDataBulkBtn.withText(path)); await t.click(workbenchPage.uploadDataBulkApplyBtn); // Verify that user can see standard error messages when any error occurs while finding the file or parsing it await t.expect(workbenchPage.Toast.toastError.textContent).contains('Data file was not found', 'Bulk upload not failed'); From 2d76fd0cb1cf741aa9226b5decfc2854342828c4 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 14:09:34 +0200 Subject: [PATCH 29/32] check --- tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index ebdb9f256d..b9ddfcab50 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -180,6 +180,7 @@ test await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('Enablement area is not visible after clicked'); // Verify that user can bulk upload data by relative path + await t.expect(workbenchPage.uploadDataBulkBtn.withText('Upload relative').visible).ok('Upload button not visible'); await t.click(workbenchPage.uploadDataBulkBtn.withText('Upload relative')); await t.click(workbenchPage.uploadDataBulkApplyBtn); // Verify that user can see the summary when the command execution is completed From 30769e3647efdec63b1ceda84af6b195998ad7cd Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 4 May 2023 15:18:21 +0200 Subject: [PATCH 30/32] return back --- .circleci/config.yml | 4 ++-- tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts | 3 +-- tests/e2e/web.runner.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c8552f0baa..ecb4bbfd9c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -389,7 +389,7 @@ jobs: description: Number of threads to run tests type: integer default: 1 - parallelism: 1 + parallelism: << parameters.parallelism >> steps: - checkout - when: @@ -1086,7 +1086,7 @@ workflows: - e2e-tests: name: E2ETest build: docker - parallelism: 1 + parallelism: 4 requires: - Build docker image diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index b9ddfcab50..e55efc2b30 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -29,7 +29,7 @@ const verifyCompletedResultText = async(resultsText: string[]): Promise => await t.click(workbenchPage.Toast.toastSubmitBtn); }; -fixture.only `Upload custom tutorials` +fixture `Upload custom tutorials` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { @@ -180,7 +180,6 @@ test await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('Enablement area is not visible after clicked'); // Verify that user can bulk upload data by relative path - await t.expect(workbenchPage.uploadDataBulkBtn.withText('Upload relative').visible).ok('Upload button not visible'); await t.click(workbenchPage.uploadDataBulkBtn.withText('Upload relative')); await t.click(workbenchPage.uploadDataBulkApplyBtn); // Verify that user can see the summary when the command execution is completed diff --git a/tests/e2e/web.runner.ts b/tests/e2e/web.runner.ts index 9778163612..823fe9f65c 100644 --- a/tests/e2e/web.runner.ts +++ b/tests/e2e/web.runner.ts @@ -41,7 +41,7 @@ import testcafe from 'testcafe'; selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, - quarantineMode: { successThreshold: '1', attemptLimit: '1' } + quarantineMode: { successThreshold: '1', attemptLimit: '3' } }); }) .then((failedCount) => { From 7cfee7dc2ddcd23e58d88a8f3f35ddd45683bace Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Fri, 5 May 2023 12:36:34 +0200 Subject: [PATCH 31/32] #RI-4494 - fix url parsing for docker builds --- .../utils/transform/remarkImage.ts | 4 +- .../utils/transform/remarkRedisUpload.ts | 4 +- .../ui/src/services/resourcesService.ts | 4 ++ redisinsight/ui/src/utils/pathUtil.ts | 51 +++++++++---------- .../ui/src/utils/tests/pathUtil.spec.ts | 6 +-- 5 files changed, 36 insertions(+), 33 deletions(-) diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkImage.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkImage.ts index 1943ebbd3a..1c86d431ab 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkImage.ts +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkImage.ts @@ -1,9 +1,9 @@ import { visit } from 'unist-util-visit' -import { prepareTutorialDataFileUrlFromMd } from 'uiSrc/utils/pathUtil' +import { getFileUrlFromMd } from 'uiSrc/utils/pathUtil' export const remarkImage = (path: string): (tree: Node) => void => (tree: any) => { // Find img node in syntax tree visit(tree, 'image', (node) => { - node.url = prepareTutorialDataFileUrlFromMd(node.url, path) + node.url = getFileUrlFromMd(node.url, path) }) } diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts index 54ce480901..056f89ed35 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/transform/remarkRedisUpload.ts @@ -1,5 +1,5 @@ import { visit } from 'unist-util-visit' -import { prepareTutorialDataFileUrlFromMd } from 'uiSrc/utils/pathUtil' +import { getFileUrlFromMd } from 'uiSrc/utils/pathUtil' export const remarkRedisUpload = (path: string): (tree: Node) => void => (tree: any) => { // Find code node in syntax tree @@ -10,7 +10,7 @@ export const remarkRedisUpload = (path: string): (tree: Node) => void => (tree: const value: string = `${lang} ${meta}` const [, filePath, label] = value.match(/^redis-upload:\[(.*)] (.*)/i) || [] - const { pathname } = new URL(prepareTutorialDataFileUrlFromMd(filePath, path)) + const { pathname } = new URL(getFileUrlFromMd(filePath, path)) const decodedPath = decodeURI(pathname) if (path && label) { diff --git a/redisinsight/ui/src/services/resourcesService.ts b/redisinsight/ui/src/services/resourcesService.ts index b174b2b3ac..eafc06d145 100644 --- a/redisinsight/ui/src/services/resourcesService.ts +++ b/redisinsight/ui/src/services/resourcesService.ts @@ -13,6 +13,10 @@ const resourcesService = axios.create({ baseURL: RESOURCES_BASE_URL, }) +export const getOriginUrl = () => (IS_ABSOLUTE_PATH.test(RESOURCES_BASE_URL) + ? RESOURCES_BASE_URL + : (window?.location?.origin || RESOURCES_BASE_URL)) + export const getPathToResource = (url: string = ''): string => (IS_ABSOLUTE_PATH.test(url) ? url : new URL(url, resourcesService.defaults.baseURL).toString()) diff --git a/redisinsight/ui/src/utils/pathUtil.ts b/redisinsight/ui/src/utils/pathUtil.ts index 3fc6a7e4ae..d9305955df 100644 --- a/redisinsight/ui/src/utils/pathUtil.ts +++ b/redisinsight/ui/src/utils/pathUtil.ts @@ -1,4 +1,4 @@ -import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService' +import { getOriginUrl } from 'uiSrc/services/resourcesService' import { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex' enum TutorialsPaths { @@ -7,37 +7,36 @@ enum TutorialsPaths { Tutorials = 'tutorials', } -export const prepareTutorialDataFileUrlFromMd = (nodeUrl: string, mdPath: string): string => { - // process external link - if (IS_ABSOLUTE_PATH.test(nodeUrl)) { - return nodeUrl +export const getRootStaticPath = (mdPath: string) => { + const paths = mdPath?.split('/') || [] + const tutorialFolder = paths[1] + + if (tutorialFolder === TutorialsPaths.CustomTutorials) return paths.slice(0, 3).join('/') + if (tutorialFolder === TutorialsPaths.Guide || tutorialFolder === TutorialsPaths.Tutorials) { + return paths.slice(0, 2).join('/') } - // process absolute path + return mdPath +} + +const processAbsolutePath = (nodeUrl: string, mdPath: string) => { + // todo: quick fix. find the root cause why path has both '/' and '\' + const normalizedMdPath = mdPath.replaceAll('\\', '/') + const tutorialRootPath = getRootStaticPath(normalizedMdPath) + + return new URL(tutorialRootPath + nodeUrl, getOriginUrl()).toString() +} + +export const getFileUrlFromMd = (nodeUrl: string, mdPath: string): string => { + // process external link + if (IS_ABSOLUTE_PATH.test(nodeUrl)) return nodeUrl + if (nodeUrl.startsWith('/') || nodeUrl.startsWith('\\')) { - // todo: quick fix. find the root cause why path has both '/' and '\' - const normalizedMdPath = mdPath.replaceAll('\\', '/') - - const paths = normalizedMdPath?.split('/') || [] - let tutorialRootPath - switch (paths[1]) { - case TutorialsPaths.CustomTutorials: - tutorialRootPath = paths.slice(0, 3).join('/') - break - case TutorialsPaths.Guide: - case TutorialsPaths.Tutorials: - tutorialRootPath = paths.slice(0, 2).join('/') - break - default: - tutorialRootPath = normalizedMdPath - break - } - - return new URL(tutorialRootPath + nodeUrl, RESOURCES_BASE_URL).toString() + return processAbsolutePath(nodeUrl, mdPath) } // process relative path - const pathUrl = new URL(mdPath, RESOURCES_BASE_URL) + const pathUrl = new URL(mdPath, getOriginUrl()) return new URL(nodeUrl, pathUrl).toString() } diff --git a/redisinsight/ui/src/utils/tests/pathUtil.spec.ts b/redisinsight/ui/src/utils/tests/pathUtil.spec.ts index f9aacc55fb..66619a44c4 100644 --- a/redisinsight/ui/src/utils/tests/pathUtil.spec.ts +++ b/redisinsight/ui/src/utils/tests/pathUtil.spec.ts @@ -1,5 +1,5 @@ import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService' -import { prepareTutorialDataFileUrlFromMd } from '../pathUtil' +import { getFileUrlFromMd } from '../pathUtil' jest.mock('unist-util-visit') const TUTORIAL_PATH = 'static/custom-tutorials/tutorial-id' @@ -31,10 +31,10 @@ const testCases = [ result: 'https://somesite.test/image.png', } ] -describe('prepareTutorialDataFileUrlFromMd', () => { +describe('getFileUrlFromMd', () => { testCases.forEach((tc) => { it(`should return ${tc.result} for url:${tc.url}, path: ${tc.path} `, () => { - const url = prepareTutorialDataFileUrlFromMd(tc.url, tc.path) + const url = getFileUrlFromMd(tc.url, tc.path) expect(url).toEqual(tc.result) }) }) From 19a408651eef0bd68f216943073c1c2c6fbb2147 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Fri, 5 May 2023 14:15:23 +0200 Subject: [PATCH 32/32] update for time taken value --- tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index e55efc2b30..f2122892ef 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -25,7 +25,7 @@ const verifyCompletedResultText = async(resultsText: string[]): Promise => for (const result of resultsText) { await t.expect(workbenchPage.Toast.toastBody.textContent).contains(result, 'Bulk upload completed summary not correct'); } - await t.expect(workbenchPage.Toast.toastBody.textContent).notContains('0:00:00.00', 'Bulk upload Time taken not correct'); + await t.expect(workbenchPage.Toast.toastBody.textContent).notContains('0:00:00.000', 'Bulk upload Time taken not correct'); await t.click(workbenchPage.Toast.toastSubmitBtn); };