From 8478e4023713caa54b9ab3b573259cd0ebd0af69 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 7 Jun 2023 13:05:06 +0700 Subject: [PATCH 001/106] RI-4592 - add triggered functions endpoint --- redisinsight/api/src/app.module.ts | 2 + redisinsight/api/src/app.routes.ts | 5 ++ .../dto/get-triggered-functions.dto.ts | 60 +++++++++++++++ .../modules/triggered-functions/dto/index.ts | 1 + .../triggered-functions/models/function.ts | 77 +++++++++++++++++++ .../triggered-functions/models/index.ts | 1 + .../triggered-functions.controller.ts | 36 +++++++++ .../triggered-functions.module.ts | 11 +++ .../triggered-functions.service.ts | 47 +++++++++++ .../triggered-functions/utils/index.ts | 1 + .../utils/triggered-functions.util.ts | 43 +++++++++++ 11 files changed, 284 insertions(+) create mode 100644 redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/dto/index.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/models/function.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/models/index.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/triggered-functions.module.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/utils/index.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts diff --git a/redisinsight/api/src/app.module.ts b/redisinsight/api/src/app.module.ts index 4a4fd36a07..fb6c427fae 100644 --- a/redisinsight/api/src/app.module.ts +++ b/redisinsight/api/src/app.module.ts @@ -15,6 +15,7 @@ import { NotificationModule } from 'src/modules/notification/notification.module import { BulkActionsModule } from 'src/modules/bulk-actions/bulk-actions.module'; import { ClusterMonitorModule } from 'src/modules/cluster-monitor/cluster-monitor.module'; import { DatabaseAnalysisModule } from 'src/modules/database-analysis/database-analysis.module'; +import { TriggeredFunctionsModule } from 'src/modules/triggered-functions/triggered-functions.module'; import { ServerModule } from 'src/modules/server/server.module'; import { LocalDatabaseModule } from 'src/local-database.module'; import { CoreModule } from 'src/core.module'; @@ -57,6 +58,7 @@ const PATH_CONFIG = config.get('dir_path'); CustomTutorialModule.register(), DatabaseAnalysisModule, DatabaseImportModule, + TriggeredFunctionsModule, ...(SERVER_CONFIG.staticContent ? [ ServeStaticModule.forRoot({ diff --git a/redisinsight/api/src/app.routes.ts b/redisinsight/api/src/app.routes.ts index 4d808cf646..7e6901972d 100644 --- a/redisinsight/api/src/app.routes.ts +++ b/redisinsight/api/src/app.routes.ts @@ -6,6 +6,7 @@ import { SlowLogModule } from 'src/modules/slow-log/slow-log.module'; import { PubSubModule } from 'src/modules/pub-sub/pub-sub.module'; import { ClusterMonitorModule } from 'src/modules/cluster-monitor/cluster-monitor.module'; import { DatabaseAnalysisModule } from 'src/modules/database-analysis/database-analysis.module'; +import { TriggeredFunctionsModule } from 'src/modules/triggered-functions/triggered-functions.module'; import { BulkActionsModule } from 'src/modules/bulk-actions/bulk-actions.module'; import { DatabaseRecommendationModule } from 'src/modules/database-recommendation/database-recommendation.module'; @@ -49,6 +50,10 @@ export const routes: Routes = [ path: '/:dbInstance', module: DatabaseRecommendationModule, }, + { + path: '/:dbInstance', + module: TriggeredFunctionsModule, + }, ], }, ]; diff --git a/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts b/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts new file mode 100644 index 0000000000..87e5dddebb --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts @@ -0,0 +1,60 @@ +import { Expose } from 'class-transformer'; +import { IsArray, IsString, IsNumber } from 'class-validator'; +import { Function } from 'src/modules/triggered-functions/models'; +import { ApiProperty } from '@nestjs/swagger'; + +export class GetTriggeredFunctionsDto { + @ApiProperty({ + description: 'Library name', + type: String, + example: 'name', + }) + @IsString() + @Expose() + name: string; + + @ApiProperty({ + description: 'Library apy version', + type: String, + example: '1.0', + }) + @IsString() + @Expose() + apiVersion: string; + + @ApiProperty({ + description: 'User name', + type: String, + example: 'default', + }) + @IsString() + @Expose() + user: string; + + @ApiProperty({ + description: 'Total of pending jobs', + type: Number, + example: 0, + }) + @IsNumber() + @Expose() + pendingJobs: number; + + @ApiProperty({ + description: 'Library configuration', + type: String, + example: 0, + }) + @IsString() + @Expose() + configuration: string; + + @ApiProperty({ + description: 'Array of functions', + isArray: true, + type: () => Function, + }) + @IsArray() + @Expose() + functions: Function[]; +} diff --git a/redisinsight/api/src/modules/triggered-functions/dto/index.ts b/redisinsight/api/src/modules/triggered-functions/dto/index.ts new file mode 100644 index 0000000000..da0ef862a0 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/dto/index.ts @@ -0,0 +1 @@ +export * from './get-triggered-functions.dto'; diff --git a/redisinsight/api/src/modules/triggered-functions/models/function.ts b/redisinsight/api/src/modules/triggered-functions/models/function.ts new file mode 100644 index 0000000000..6164891dcd --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/models/function.ts @@ -0,0 +1,77 @@ +import { Expose } from 'class-transformer'; +import { + IsArray, IsEnum, IsOptional, IsString, IsBoolean, IsNumber, +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum FunctionType { + Function = 'functions', + ClusterFunction = 'cluster_functions', + KeyspaceTrigger = 'keyspace_triggers', + StreamTrigger = 'stream_triggers', +} + +export class Function { + @ApiProperty({ + description: 'Function type', + enum: FunctionType, + }) + @IsEnum(FunctionType) + @Expose() + type: FunctionType; + + @ApiProperty({ + description: 'Function name', + type: String, + example: 'name', + }) + @IsString() + @Expose() + name: string; + + @ApiPropertyOptional({ + description: 'Total succeed function', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + success?: number; + + @ApiPropertyOptional({ + description: 'Total failed function', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + fail?: number; + + @ApiPropertyOptional({ + description: 'Total trigger function', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + total?: number; + + @ApiPropertyOptional({ + description: 'Function flags', + type: String, + isArray: true, + }) + @IsArray() + @Expose() + flags?: string[]; + + @ApiPropertyOptional({ + description: 'Is functions is async', + type: Number, + example: 0, + }) + @IsNumber() + @IsOptional() + @Expose() + is_async?: number; +} diff --git a/redisinsight/api/src/modules/triggered-functions/models/index.ts b/redisinsight/api/src/modules/triggered-functions/models/index.ts new file mode 100644 index 0000000000..2653adb2a8 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/models/index.ts @@ -0,0 +1 @@ +export * from './function'; diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts new file mode 100644 index 0000000000..f332d1203e --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts @@ -0,0 +1,36 @@ +import { + Get, + Controller, UsePipes, ValidationPipe, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; +import { TriggeredFunctionsService } from 'src/modules/triggered-functions/triggered-functions.service'; +import { GetTriggeredFunctionsDto } from 'src/modules/triggered-functions/dto'; +import { PublishResponse } from 'src/modules/pub-sub/dto/publish.response'; +import { ClientMetadata } from 'src/common/models'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; + +@ApiTags('Triggered Functions') +@Controller('triggered-functions') +@UsePipes(new ValidationPipe()) +export class TriggeredFunctionsController { + constructor(private service: TriggeredFunctionsService) {} + + @Get('') + @ApiRedisInstanceOperation({ + description: 'Returns libraries', + statusCode: 200, + responses: [ + { + status: 200, + description: 'Returns libraries', + type: PublishResponse, + }, + ], + }) + async list( + @BrowserClientMetadata() clientMetadata: ClientMetadata, + ): Promise { + return this.service.list(clientMetadata); + } +} diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.module.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.module.ts new file mode 100644 index 0000000000..92cefc918b --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TriggeredFunctionsController } from 'src/modules/triggered-functions/triggered-functions.controller'; +import { TriggeredFunctionsService } from 'src/modules/triggered-functions/triggered-functions.service'; + +@Module({ + controllers: [TriggeredFunctionsController], + providers: [ + TriggeredFunctionsService, + ], +}) +export class TriggeredFunctionsModule {} diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts new file mode 100644 index 0000000000..cb234809f7 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts @@ -0,0 +1,47 @@ +import { Command } from 'ioredis'; +import { HttpException, Injectable, Logger } from '@nestjs/common'; +import { catchAclError, classToClass } from 'src/utils'; +import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { GetTriggeredFunctionsDto } from 'src/modules/triggered-functions/dto'; +import { getLibraryInformation } from 'src/modules/triggered-functions/utils'; +import { ClientMetadata } from 'src/common/models'; + +@Injectable() +export class TriggeredFunctionsService { + private logger = new Logger('TriggeredFunctionsService'); + + constructor( + private readonly databaseConnectionService: DatabaseConnectionService, + ) {} + + /** + * Get analysis list for particular database with id and createdAt fields only + * @param clientMetadata + */ + async list( + clientMetadata: ClientMetadata, + ): Promise { + let client; + try { + client = await this.databaseConnectionService.createClient(clientMetadata); + const reply = await client.sendCommand( + new Command('TFUNCTION', ['LIST', 'vvv'], { replyEncoding: 'utf8' }), + ); + + client.disconnect(); + return classToClass( + GetTriggeredFunctionsDto, + reply.map((lib: string[]) => getLibraryInformation(lib)), + ); + } catch (e) { + client?.disconnect(); + this.logger.error('Unable to get database triggered functions', e); + + if (e instanceof HttpException) { + throw e; + } + + throw catchAclError(e); + } + } +} diff --git a/redisinsight/api/src/modules/triggered-functions/utils/index.ts b/redisinsight/api/src/modules/triggered-functions/utils/index.ts new file mode 100644 index 0000000000..e7237f85ab --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/utils/index.ts @@ -0,0 +1 @@ +export * from './triggered-functions.util'; diff --git a/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts new file mode 100644 index 0000000000..aab7e96a3e --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts @@ -0,0 +1,43 @@ +import { concat } from 'lodash'; +import { convertStringsArrayToObject } from 'src/utils'; +import { FunctionType, Function } from 'src/modules/triggered-functions/models'; + +/** + * Get all functions +*/ +const getFunctionsInformation = (functions: string[][], type: FunctionType): Function[] => functions.map((reply) => { + const func = convertStringsArrayToObject(reply); + return ({ + name: func.name, + success: func.num_success, + fail: func.num_failed, + total: func.num_trigger, + isAsync: func.is_async, + flags: func.flags, + type, + }); +}); + +/** + * Get all functions +*/ +const collectFunctions = (lib) => { + const functionGroups = Object.values(FunctionType).map((type) => getFunctionsInformation(lib[type], type)); + return concat(...functionGroups); +}; + +/** + * Get library information +*/ +export const getLibraryInformation = (lib: string[]) => { + const library = convertStringsArrayToObject(lib); + const functions = collectFunctions(library); + return ({ + name: library.name, + apiVersion: library.api_version, + user: library.user, + pendingJobs: library.pending_jobs, + configuration: library.configuration, + functions, + }); +}; From e0a9f56c7cffce6be33bbeb31f5099dded9d6eac Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 7 Jun 2023 14:09:43 +0700 Subject: [PATCH 002/106] RI-4592 - add triggered functions endpoint --- .../dto/get-triggered-functions.dto.ts | 8 +++++--- .../src/modules/triggered-functions/models/function.ts | 10 ++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts b/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts index 87e5dddebb..b694db345b 100644 --- a/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts +++ b/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts @@ -1,5 +1,5 @@ -import { Expose } from 'class-transformer'; -import { IsArray, IsString, IsNumber } from 'class-validator'; +import { Expose, Type } from 'class-transformer'; +import { IsArray, IsString, IsNumber, ValidateNested } from 'class-validator'; import { Function } from 'src/modules/triggered-functions/models'; import { ApiProperty } from '@nestjs/swagger'; @@ -52,9 +52,11 @@ export class GetTriggeredFunctionsDto { @ApiProperty({ description: 'Array of functions', isArray: true, - type: () => Function, + type: Function, }) @IsArray() + @ValidateNested({ each: true }) + @Type(() => Function) @Expose() functions: Function[]; } diff --git a/redisinsight/api/src/modules/triggered-functions/models/function.ts b/redisinsight/api/src/modules/triggered-functions/models/function.ts index 6164891dcd..f5da580f23 100644 --- a/redisinsight/api/src/modules/triggered-functions/models/function.ts +++ b/redisinsight/api/src/modules/triggered-functions/models/function.ts @@ -1,7 +1,8 @@ -import { Expose } from 'class-transformer'; +import { Expose, Transform } from 'class-transformer'; import { IsArray, IsEnum, IsOptional, IsString, IsBoolean, IsNumber, } from 'class-validator'; +import { isNumber } from 'lodash'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export enum FunctionType { @@ -67,11 +68,12 @@ export class Function { @ApiPropertyOptional({ description: 'Is functions is async', - type: Number, + type: Boolean, example: 0, }) - @IsNumber() + @IsBoolean() @IsOptional() @Expose() - is_async?: number; + @Transform((val) => isNumber(val) ? val === 1 : undefined) + isAsync?: boolean; } From 9eadbd4a4e1e2a93411ca430e31908d9f684005f Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 8 Jun 2023 13:33:23 +0700 Subject: [PATCH 003/106] #RI-4592 - add code field --- .../modules/triggered-functions/triggered-functions.service.ts | 2 +- .../triggered-functions/utils/triggered-functions.util.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts index cb234809f7..1025326a22 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts @@ -25,7 +25,7 @@ export class TriggeredFunctionsService { try { client = await this.databaseConnectionService.createClient(clientMetadata); const reply = await client.sendCommand( - new Command('TFUNCTION', ['LIST', 'vvv'], { replyEncoding: 'utf8' }), + new Command('TFUNCTION', ['LIST', 'WITHCODE', 'vvv'], { replyEncoding: 'utf8' }), ); client.disconnect(); diff --git a/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts index aab7e96a3e..ed45b0e72c 100644 --- a/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts +++ b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts @@ -38,6 +38,7 @@ export const getLibraryInformation = (lib: string[]) => { user: library.user, pendingJobs: library.pending_jobs, configuration: library.configuration, + code: library.code, functions, }); }; From ff5217a174af8f971f69bec0c346e74911704245 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 8 Jun 2023 14:31:08 +0700 Subject: [PATCH 004/106] #RI-4592 - add code field --- .../dto/get-triggered-functions.dto.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts b/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts index b694db345b..cac10796f9 100644 --- a/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts +++ b/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts @@ -49,6 +49,15 @@ export class GetTriggeredFunctionsDto { @Expose() configuration: string; + @ApiProperty({ + description: 'Library code', + type: String, + example: 0, + }) + @IsString() + @Expose() + code: string; + @ApiProperty({ description: 'Array of functions', isArray: true, From e528e112f83d55679ffa3aa212abef7055837626 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 8 Jun 2023 16:09:49 +0700 Subject: [PATCH 005/106] #RI-4592 - add stream triggers --- .../triggered-functions/models/function.ts | 79 ++++++++++++++++++- .../utils/triggered-functions.util.ts | 20 ++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/redisinsight/api/src/modules/triggered-functions/models/function.ts b/redisinsight/api/src/modules/triggered-functions/models/function.ts index f5da580f23..a80fb4922c 100644 --- a/redisinsight/api/src/modules/triggered-functions/models/function.ts +++ b/redisinsight/api/src/modules/triggered-functions/models/function.ts @@ -74,6 +74,83 @@ export class Function { @IsBoolean() @IsOptional() @Expose() - @Transform((val) => isNumber(val) ? val === 1 : undefined) + @Transform((val) => (isNumber(val) ? val === 1 : undefined)) isAsync?: boolean; + + @ApiPropertyOptional({ + description: 'Function description', + type: String, + example: 'some description', + }) + @IsString() + @IsOptional() + @Expose() + description?: string; + + @ApiPropertyOptional({ + description: 'Last execution error', + type: String, + example: 'error', + }) + @IsString() + @IsOptional() + @Expose() + lastError?: string; + + @ApiPropertyOptional({ + description: 'Last function execution time', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + lastExecutionTime?: number; + + @ApiPropertyOptional({ + description: 'Total execution time', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + totalExecutionTime?: number; + + @ApiPropertyOptional({ + description: 'Stream trigger prefix', + type: String, + example: 'stream', + }) + @IsString() + @IsOptional() + @Expose() + prefix?: string; + + @ApiPropertyOptional({ + description: 'Whether or not to trim the stream', + type: Boolean, + example: true, + }) + @IsBoolean() + @IsOptional() + @Transform((val) => (isNumber(val) ? val === 1 : undefined)) + @Expose() + trim?: boolean; + + @ApiPropertyOptional({ + description: 'How many elements can be processed simultaneously', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + window?: number; + + @ApiPropertyOptional({ + description: 'Stream triggers streams', + isArray: true, + }) + @IsArray() + @IsOptional() + @Expose() + streams?: any[]; } diff --git a/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts index ed45b0e72c..6db47695f1 100644 --- a/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts +++ b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts @@ -5,8 +5,19 @@ import { FunctionType, Function } from 'src/modules/triggered-functions/models'; /** * Get all functions */ -const getFunctionsInformation = (functions: string[][], type: FunctionType): Function[] => functions.map((reply) => { +const getFunctionsInformation = ( + functions: string[][] | string[], + type: FunctionType, +): Function[] => functions.map((reply) => { + if (type === FunctionType.ClusterFunction) { + return ({ + name: reply as string, + type, + }); + } + const func = convertStringsArrayToObject(reply); + return ({ name: func.name, success: func.num_success, @@ -14,6 +25,13 @@ const getFunctionsInformation = (functions: string[][], type: FunctionType): Fun total: func.num_trigger, isAsync: func.is_async, flags: func.flags, + lastError: func.last_error, + lastExecutionTime: func.last_execution_time, + totalExecutionTime: func.total_execution_time, + prefix: func.prefix, + streams: func.streams?.map((stream) => convertStringsArrayToObject(stream)), + trim: func.trim, + window: func.window, type, }); }); From ebcf5b7d80ca2ffc8ff72d1f7601a8094010b394 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 9 Jun 2023 11:41:01 +0700 Subject: [PATCH 006/106] #RI-4592 - update endpoints --- .../modules/triggered-functions/dto/index.ts | 2 +- .../triggered-functions/dto/library.dto.ts | 17 ++++ .../triggered-functions/models/function.ts | 9 ++ .../triggered-functions/models/index.ts | 2 + .../library.ts} | 15 +++- .../models/short-library.ts | 6 ++ .../triggered-functions.controller.ts | 56 ++++++++++-- .../triggered-functions.service.ts | 87 ++++++++++++++++--- .../utils/triggered-functions.util.ts | 45 +++++++++- 9 files changed, 215 insertions(+), 24 deletions(-) create mode 100644 redisinsight/api/src/modules/triggered-functions/dto/library.dto.ts rename redisinsight/api/src/modules/triggered-functions/{dto/get-triggered-functions.dto.ts => models/library.ts} (81%) create mode 100644 redisinsight/api/src/modules/triggered-functions/models/short-library.ts diff --git a/redisinsight/api/src/modules/triggered-functions/dto/index.ts b/redisinsight/api/src/modules/triggered-functions/dto/index.ts index da0ef862a0..1f35351615 100644 --- a/redisinsight/api/src/modules/triggered-functions/dto/index.ts +++ b/redisinsight/api/src/modules/triggered-functions/dto/index.ts @@ -1 +1 @@ -export * from './get-triggered-functions.dto'; +export * from './library.dto'; diff --git a/redisinsight/api/src/modules/triggered-functions/dto/library.dto.ts b/redisinsight/api/src/modules/triggered-functions/dto/library.dto.ts new file mode 100644 index 0000000000..4ed8086311 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/dto/library.dto.ts @@ -0,0 +1,17 @@ +import { + IsDefined, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { RedisString } from 'src/common/constants'; +import { IsRedisString, RedisStringType } from 'src/common/decorators'; + +export class LibraryDto { + @ApiProperty({ + description: 'Key Name', + type: String, + }) + @IsDefined() + // @IsRedisString() + // @RedisStringType() + libraryName: string; +} diff --git a/redisinsight/api/src/modules/triggered-functions/models/function.ts b/redisinsight/api/src/modules/triggered-functions/models/function.ts index a80fb4922c..f0af91fa39 100644 --- a/redisinsight/api/src/modules/triggered-functions/models/function.ts +++ b/redisinsight/api/src/modules/triggered-functions/models/function.ts @@ -30,6 +30,15 @@ export class Function { @Expose() name: string; + @ApiPropertyOptional({ + description: 'Library name', + type: String, + example: 0, + }) + @IsString() + @Expose() + library?: string; + @ApiPropertyOptional({ description: 'Total succeed function', type: Number, diff --git a/redisinsight/api/src/modules/triggered-functions/models/index.ts b/redisinsight/api/src/modules/triggered-functions/models/index.ts index 2653adb2a8..43013c91ff 100644 --- a/redisinsight/api/src/modules/triggered-functions/models/index.ts +++ b/redisinsight/api/src/modules/triggered-functions/models/index.ts @@ -1 +1,3 @@ export * from './function'; +export * from './short-library'; +export * from './library'; diff --git a/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts b/redisinsight/api/src/modules/triggered-functions/models/library.ts similarity index 81% rename from redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts rename to redisinsight/api/src/modules/triggered-functions/models/library.ts index cac10796f9..f2812a5626 100644 --- a/redisinsight/api/src/modules/triggered-functions/dto/get-triggered-functions.dto.ts +++ b/redisinsight/api/src/modules/triggered-functions/models/library.ts @@ -1,9 +1,11 @@ import { Expose, Type } from 'class-transformer'; -import { IsArray, IsString, IsNumber, ValidateNested } from 'class-validator'; +import { + IsArray, IsString, IsNumber, ValidateNested, +} from 'class-validator'; import { Function } from 'src/modules/triggered-functions/models'; import { ApiProperty } from '@nestjs/swagger'; -export class GetTriggeredFunctionsDto { +export class LibraryInformation { @ApiProperty({ description: 'Library name', type: String, @@ -68,4 +70,13 @@ export class GetTriggeredFunctionsDto { @Type(() => Function) @Expose() functions: Function[]; + + @ApiProperty({ + description: 'Total number of functions', + type: Number, + example: 0, + }) + @IsNumber() + @Expose() + totalFunctions: number; } diff --git a/redisinsight/api/src/modules/triggered-functions/models/short-library.ts b/redisinsight/api/src/modules/triggered-functions/models/short-library.ts new file mode 100644 index 0000000000..f496dbe23b --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/models/short-library.ts @@ -0,0 +1,6 @@ +import { PartialType, PickType } from '@nestjs/swagger'; +import { LibraryInformation } from 'src/modules/triggered-functions/models/library'; + +export class ShortLibraryInformation extends PartialType( + PickType(LibraryInformation, ['name', 'user', 'totalFunctions', 'pendingJobs'] as const), +) {} diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts index f332d1203e..5d47fa47f1 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts @@ -1,12 +1,13 @@ import { Get, - Controller, UsePipes, ValidationPipe, + Post, + Controller, UsePipes, ValidationPipe, Body, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; import { TriggeredFunctionsService } from 'src/modules/triggered-functions/triggered-functions.service'; -import { GetTriggeredFunctionsDto } from 'src/modules/triggered-functions/dto'; -import { PublishResponse } from 'src/modules/pub-sub/dto/publish.response'; +import { ShortLibraryInformation, LibraryInformation, Function } from 'src/modules/triggered-functions/models'; +import { LibraryDto } from 'src/modules/triggered-functions/dto'; import { ClientMetadata } from 'src/common/models'; import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; @@ -16,21 +17,58 @@ import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-cl export class TriggeredFunctionsController { constructor(private service: TriggeredFunctionsService) {} - @Get('') + @Get('/libraries') @ApiRedisInstanceOperation({ - description: 'Returns libraries', + description: 'Returns short libraries information', statusCode: 200, responses: [ { status: 200, description: 'Returns libraries', - type: PublishResponse, + type: ShortLibraryInformation, }, ], }) - async list( + async libraryList( @BrowserClientMetadata() clientMetadata: ClientMetadata, - ): Promise { - return this.service.list(clientMetadata); + ): Promise { + return this.service.libraryList(clientMetadata); + } + + @Post('/get-library') + @ApiRedisInstanceOperation({ + description: 'Returns library information', + statusCode: 200, + responses: [ + { + status: 200, + description: 'Returns library information', + type: LibraryInformation, + }, + ], + }) + async details( + @BrowserClientMetadata() clientMetadata: ClientMetadata, + @Body() dto: LibraryDto, + ): Promise { + return this.service.details(clientMetadata, dto.libraryName); + } + + @Get('/functions') + @ApiRedisInstanceOperation({ + description: 'Returns function information', + statusCode: 200, + responses: [ + { + status: 200, + description: 'Returns functions', + type: Function, + }, + ], + }) + async functionsList( + @BrowserClientMetadata() clientMetadata: ClientMetadata, + ): Promise { + return this.service.functionsList(clientMetadata); } } diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts index 1025326a22..90f72b2031 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts @@ -1,9 +1,10 @@ import { Command } from 'ioredis'; import { HttpException, Injectable, Logger } from '@nestjs/common'; +import { concat } from 'lodash'; import { catchAclError, classToClass } from 'src/utils'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; -import { GetTriggeredFunctionsDto } from 'src/modules/triggered-functions/dto'; -import { getLibraryInformation } from 'src/modules/triggered-functions/utils'; +import { ShortLibraryInformation, LibraryInformation, Function } from 'src/modules/triggered-functions/models'; +import { getLibraryInformation, getShortLibraryInformation, getFunctions } from 'src/modules/triggered-functions/utils'; import { ClientMetadata } from 'src/common/models'; @Injectable() @@ -15,27 +16,93 @@ export class TriggeredFunctionsService { ) {} /** - * Get analysis list for particular database with id and createdAt fields only + * Get library list for particular database with name, user, totalFunctions, pendingJobs fields only * @param clientMetadata */ - async list( + async libraryList( clientMetadata: ClientMetadata, - ): Promise { + ): Promise { let client; try { client = await this.databaseConnectionService.createClient(clientMetadata); const reply = await client.sendCommand( - new Command('TFUNCTION', ['LIST', 'WITHCODE', 'vvv'], { replyEncoding: 'utf8' }), + new Command('TFUNCTION', ['LIST'], { replyEncoding: 'utf8' }), ); + client.disconnect(); + const libraries = reply.map((lib: string[]) => getShortLibraryInformation(lib)); + return libraries.map((lib) => classToClass( + ShortLibraryInformation, + lib, + )); + } catch (e) { + client?.disconnect(); + this.logger.error('Unable to get database libraries', e); + + if (e instanceof HttpException) { + throw e; + } + + throw catchAclError(e); + } + } + /** + * Get library details + * @param clientMetadata + * @param name + */ + async details( + clientMetadata: ClientMetadata, + name: string, + ): Promise { + let client; + try { + client = await this.databaseConnectionService.createClient(clientMetadata); + const reply = await client.sendCommand( + new Command('TFUNCTION', ['LIST', 'WITHCODE', 'LIBRARY', name], { replyEncoding: 'utf8' }), + ); client.disconnect(); - return classToClass( - GetTriggeredFunctionsDto, - reply.map((lib: string[]) => getLibraryInformation(lib)), + const libraries = reply.map((lib: string[]) => getLibraryInformation(lib)); + return libraries.map((lib) => classToClass( + LibraryInformation, + lib, + )); + } catch (e) { + client?.disconnect(); + this.logger.error('Unable to get database triggered functions libraries', e); + + if (e instanceof HttpException) { + throw e; + } + + throw catchAclError(e); + } + } + + /** + * Get library list for particular database with name, user, totalFunctions, pendingJobs fields only + * @param clientMetadata + * @param name + */ + async functionsList( + clientMetadata: ClientMetadata, + ): Promise { + let client; + try { + client = await this.databaseConnectionService.createClient(clientMetadata); + const reply = await client.sendCommand( + new Command('TFUNCTION', ['LIST', 'vvv'], { replyEncoding: 'utf8' }), ); + const functions = reply.reduce((prev, cur) => concat(prev, getFunctions(cur)), []); + client.disconnect(); + console.log(functions) + return functions.map((func) => classToClass( + Function, + func, + )); } catch (e) { client?.disconnect(); - this.logger.error('Unable to get database triggered functions', e); + this.logger.error('Unable to get database triggered functions libraries', e); if (e instanceof HttpException) { throw e; diff --git a/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts index 6db47695f1..1b48b98241 100644 --- a/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts +++ b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts @@ -8,11 +8,13 @@ import { FunctionType, Function } from 'src/modules/triggered-functions/models'; const getFunctionsInformation = ( functions: string[][] | string[], type: FunctionType, + libName: string, ): Function[] => functions.map((reply) => { if (type === FunctionType.ClusterFunction) { return ({ name: reply as string, type, + library: libName, }); } @@ -33,23 +35,49 @@ const getFunctionsInformation = ( trim: func.trim, window: func.window, type, + library: libName, }); }); +const getFunctionName = ( + functions: string[][] | string[], + type: FunctionType, +): Function[] => functions.map((reply) => ({ + name: reply as string, + type, +})); + +/** + * Get all function names +*/ +const getFunctionNames = ( + lib, +): Function[] => { + const functionGroups = Object.values(FunctionType).map((type) => getFunctionName(lib[type], type)); + return concat(...functionGroups); +}; + /** * Get all functions */ const collectFunctions = (lib) => { - const functionGroups = Object.values(FunctionType).map((type) => getFunctionsInformation(lib[type], type)); + const functionGroups = Object.values(FunctionType).map((type) => getFunctionsInformation(lib[type], type, lib.name)); return concat(...functionGroups); }; +/** + * Get functions count +*/ +const getTotalFunctions = (lib) => ( + Object.values(FunctionType).reduce((prev, cur) => prev + lib[cur].length, 0) +); + /** * Get library information */ export const getLibraryInformation = (lib: string[]) => { const library = convertStringsArrayToObject(lib); - const functions = collectFunctions(library); + const functions = getFunctionNames(library); return ({ name: library.name, apiVersion: library.api_version, @@ -60,3 +88,16 @@ export const getLibraryInformation = (lib: string[]) => { functions, }); }; + +export const getFunctions = (lib: string[]) => collectFunctions(convertStringsArrayToObject(lib)); + +export const getShortLibraryInformation = (lib: string[]) => { + const library = convertStringsArrayToObject(lib); + const totalFunctions = getTotalFunctions(library); + return ({ + name: library.name, + user: library.user, + pendingJobs: library.pending_jobs, + totalFunctions, + }); +}; From 487dcc14b30621ca85b08723d8738cb42c8ce577 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 9 Jun 2023 12:55:48 +0700 Subject: [PATCH 007/106] #RI-4592 - update get library endpoint --- .../triggered-functions/triggered-functions.controller.ts | 2 +- .../triggered-functions/triggered-functions.service.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts index 5d47fa47f1..24620f15fc 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts @@ -50,7 +50,7 @@ export class TriggeredFunctionsController { async details( @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: LibraryDto, - ): Promise { + ): Promise { return this.service.details(clientMetadata, dto.libraryName); } diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts index 90f72b2031..541f0e6cec 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts @@ -54,7 +54,7 @@ export class TriggeredFunctionsService { async details( clientMetadata: ClientMetadata, name: string, - ): Promise { + ): Promise { let client; try { client = await this.databaseConnectionService.createClient(clientMetadata); @@ -63,10 +63,10 @@ export class TriggeredFunctionsService { ); client.disconnect(); const libraries = reply.map((lib: string[]) => getLibraryInformation(lib)); - return libraries.map((lib) => classToClass( + return classToClass( LibraryInformation, - lib, - )); + libraries[0], + ); } catch (e) { client?.disconnect(); this.logger.error('Unable to get database triggered functions libraries', e); From e142127cf69330f83a97fc9a61290de3f708920d Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Fri, 9 Jun 2023 14:16:09 +0200 Subject: [PATCH 008/106] #RI-4593 - add triggers & functions libraries list --- .../ui/src/assets/img/sidebar/gears.svg | 3 + .../src/assets/img/sidebar/gears_active.svg | 3 + .../main-router/constants/defaultRoutes.ts | 8 +- .../main-router/constants/redisStackRoutes.ts | 22 +++ .../main-router/constants/sub-routes/index.ts | 4 +- .../sub-routes/triggeredFunctionsRoutes.ts | 15 ++ .../navigation-menu/NavigationMenu.tsx | 43 +++++- .../navigation-menu/styles.module.scss | 31 ++++ redisinsight/ui/src/constants/api.ts | 2 + redisinsight/ui/src/constants/pages.ts | 26 ++-- .../components/auto-refresh/AutoRefresh.tsx | 9 +- .../ui/src/pages/instance/InstancePage.tsx | 2 + .../TriggeredFunctionsPage.tsx | 59 ++++++++ .../TriggeredFunctionsPageRouter.tsx | 17 +++ .../ui/src/pages/triggeredFunctions/index.ts | 3 + .../pages/Libraries/LibrariesPage.tsx | 111 ++++++++++++++ .../LibrariesList/LibrariesList.tsx | 141 ++++++++++++++++++ .../components/LibrariesList/index.ts | 3 + .../LibrariesList/styles.module.scss | 37 +++++ .../NoLibrariesScreen/NoLibrariesScreen.tsx | 24 +++ .../components/NoLibrariesScreen/index.ts | 3 + .../NoLibrariesScreen/styles.module.scss | 27 ++++ .../pages/Libraries/index.ts | 3 + .../pages/Libraries/styles.module.scss | 24 +++ .../pages/triggeredFunctions/pages/index.ts | 5 + .../triggeredFunctions/styles.modules.scss | 21 +++ .../slices/interfaces/triggeredFunctions.ts | 22 +++ redisinsight/ui/src/slices/store.ts | 2 + .../triggeredFunctions.spec.ts | 0 .../triggeredFunctions/triggeredFunctions.ts | 78 ++++++++++ redisinsight/ui/src/telemetry/events.ts | 5 + redisinsight/ui/src/telemetry/pageViews.ts | 3 +- redisinsight/ui/src/utils/test-utils.tsx | 2 + 33 files changed, 736 insertions(+), 22 deletions(-) create mode 100644 redisinsight/ui/src/assets/img/sidebar/gears.svg create mode 100644 redisinsight/ui/src/assets/img/sidebar/gears_active.svg create mode 100644 redisinsight/ui/src/components/main-router/constants/sub-routes/triggeredFunctionsRoutes.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPageRouter.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss create mode 100644 redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts create mode 100644 redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts create mode 100644 redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts diff --git a/redisinsight/ui/src/assets/img/sidebar/gears.svg b/redisinsight/ui/src/assets/img/sidebar/gears.svg new file mode 100644 index 0000000000..1ae540a6ad --- /dev/null +++ b/redisinsight/ui/src/assets/img/sidebar/gears.svg @@ -0,0 +1,3 @@ + + + diff --git a/redisinsight/ui/src/assets/img/sidebar/gears_active.svg b/redisinsight/ui/src/assets/img/sidebar/gears_active.svg new file mode 100644 index 0000000000..a5d38d44c2 --- /dev/null +++ b/redisinsight/ui/src/assets/img/sidebar/gears_active.svg @@ -0,0 +1,3 @@ + + + diff --git a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts index dd4420bff9..62e04a0227 100644 --- a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts @@ -12,7 +12,8 @@ import { import WorkbenchPage from 'uiSrc/pages/workbench' import PubSubPage from 'uiSrc/pages/pubSub' import AnalyticsPage from 'uiSrc/pages/analytics' -import { ANALYTICS_ROUTES } from './sub-routes' +import TriggeredFunctionsPage from 'uiSrc/pages/triggeredFunctions' +import { ANALYTICS_ROUTES, TRIGGERED_FUNCTIONS_ROUTES } from './sub-routes' import COMMON_ROUTES from './commonRoutes' @@ -37,6 +38,11 @@ const INSTANCE_ROUTES: IRoute[] = [ component: AnalyticsPage, routes: ANALYTICS_ROUTES, }, + { + path: Pages.triggeredFunctions(':instanceId'), + component: TriggeredFunctionsPage, + routes: TRIGGERED_FUNCTIONS_ROUTES + } ] const ROUTES: IRoute[] = [ diff --git a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts index 406dfd3263..4efbaf0073 100644 --- a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts @@ -9,6 +9,8 @@ import EditConnection from 'uiSrc/pages/redisStack/components/edit-connection' import ClusterDetailsPage from 'uiSrc/pages/clusterDetails' import AnalyticsPage from 'uiSrc/pages/analytics' import DatabaseAnalysisPage from 'uiSrc/pages/databaseAnalysis' +import TriggeredFunctionsPage from 'uiSrc/pages/triggeredFunctions' +import { LibrariesPage } from 'uiSrc/pages/triggeredFunctions/pages' import COMMON_ROUTES from './commonRoutes' const ANALYTICS_ROUTES: IRoute[] = [ @@ -32,6 +34,21 @@ const ANALYTICS_ROUTES: IRoute[] = [ }, ] +const TRIGGERED_FUNCTIONS_ROUTES: IRoute[] = [ + { + pageName: PageNames.triggeredFunctionsFunctions, + path: Pages.triggeredFunctionsFunctions(':instanceId'), + protected: true, + component: LibrariesPage, + }, + { + pageName: PageNames.triggeredFunctionsLibraries, + path: Pages.triggeredFunctionsLibraries(':instanceId'), + protected: true, + component: LibrariesPage, + }, +] + const INSTANCE_ROUTES: IRoute[] = [ { pageName: PageNames.browser, @@ -57,6 +74,11 @@ const INSTANCE_ROUTES: IRoute[] = [ component: AnalyticsPage, routes: ANALYTICS_ROUTES, }, + { + path: Pages.triggeredFunctions(':instanceId'), + component: TriggeredFunctionsPage, + routes: TRIGGERED_FUNCTIONS_ROUTES + } ] const ROUTES: IRoute[] = [ diff --git a/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts b/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts index 7cfe9e8d2b..9bdeabe7ea 100644 --- a/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts +++ b/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts @@ -1,5 +1,7 @@ import { ANALYTICS_ROUTES } from './analyticsRoutes' +import { TRIGGERED_FUNCTIONS_ROUTES } from './triggeredFunctionsRoutes' export { - ANALYTICS_ROUTES + ANALYTICS_ROUTES, + TRIGGERED_FUNCTIONS_ROUTES } diff --git a/redisinsight/ui/src/components/main-router/constants/sub-routes/triggeredFunctionsRoutes.ts b/redisinsight/ui/src/components/main-router/constants/sub-routes/triggeredFunctionsRoutes.ts new file mode 100644 index 0000000000..f164976780 --- /dev/null +++ b/redisinsight/ui/src/components/main-router/constants/sub-routes/triggeredFunctionsRoutes.ts @@ -0,0 +1,15 @@ +import { IRoute, PageNames, Pages } from 'uiSrc/constants' +import { LibrariesPage } from 'uiSrc/pages/triggeredFunctions/pages' + +export const TRIGGERED_FUNCTIONS_ROUTES: IRoute[] = [ + { + pageName: PageNames.triggeredFunctionsFunctions, + path: Pages.triggeredFunctionsFunctions(':instanceId'), + component: LibrariesPage, + }, + { + pageName: PageNames.triggeredFunctionsLibraries, + path: Pages.triggeredFunctionsLibraries(':instanceId'), + component: LibrariesPage, + }, +] diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx index 565d13c557..b6eb7990b0 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx @@ -5,6 +5,7 @@ import cx from 'classnames' import { last } from 'lodash' import { useSelector } from 'react-redux' import { + EuiBadge, EuiButtonIcon, EuiIcon, EuiLink, @@ -12,7 +13,7 @@ import { EuiToolTip } from '@elastic/eui' import HighlightedFeature from 'uiSrc/components/hightlighted-feature/HighlightedFeature' -import { ANALYTICS_ROUTES } from 'uiSrc/components/main-router/constants/sub-routes' +import { ANALYTICS_ROUTES, TRIGGERED_FUNCTIONS_ROUTES } from 'uiSrc/components/main-router/constants/sub-routes' import { PageNames, Pages } from 'uiSrc/constants' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' @@ -33,6 +34,8 @@ import SlowLogSVG from 'uiSrc/assets/img/sidebar/slowlog.svg' import SlowLogActiveSVG from 'uiSrc/assets/img/sidebar/slowlog_active.svg' import PubSubSVG from 'uiSrc/assets/img/sidebar/pubsub.svg' import PubSubActiveSVG from 'uiSrc/assets/img/sidebar/pubsub_active.svg' +import TriggeredFunctionsSVG from 'uiSrc/assets/img/sidebar/gears.svg' +import TriggeredFunctionsActiveSVG from 'uiSrc/assets/img/sidebar/gears_active.svg' import GithubSVG from 'uiSrc/assets/img/sidebar/github.svg' import Divider from 'uiSrc/components/divider/Divider' import { BuildType } from 'uiSrc/constants/env' @@ -50,6 +53,7 @@ const pubSubPath = `/${PageNames.pubSub}` interface INavigations { isActivePage: boolean + isBeta?: boolean pageName: string tooltipText: string ariaLabel: string @@ -81,6 +85,10 @@ const NavigationMenu = () => { ({ path }) => (`/${last(path.split('/'))}` === activePage) ) + const isTriggeredFunctionsPath = (activePage: string) => !!TRIGGERED_FUNCTIONS_ROUTES.find( + ({ path }) => (`/${last(path.split('/'))}` === activePage) + ) + const privateRoutes: INavigations[] = [ { tooltipText: 'Browser', @@ -145,6 +153,22 @@ const NavigationMenu = () => { }, onboard: ONBOARDING_FEATURES.PUB_SUB_PAGE }, + { + tooltipText: 'Triggers & Functions', + pageName: PageNames.triggeredFunctions, + ariaLabel: 'Triggers & Functions', + onClick: () => handleGoPage(Pages.triggeredFunctions(connectedInstanceId)), + dataTestId: 'triggered-functions-page-btn', + connectedInstanceId, + isActivePage: isTriggeredFunctionsPath(activePage), + isBeta: true, + getClassName() { + return cx(styles.navigationButton, { [styles.active]: this.isActivePage }) + }, + getIconType() { + return this.isActivePage ? TriggeredFunctionsActiveSVG : TriggeredFunctionsSVG + }, + }, ] const publicRoutes: INavigations[] = [ @@ -190,13 +214,16 @@ const NavigationMenu = () => { transformOnHover > - +
+ + {nav.isBeta && (BETA)} +
), diff --git a/redisinsight/ui/src/components/navigation-menu/styles.module.scss b/redisinsight/ui/src/components/navigation-menu/styles.module.scss index af32fb87de..6ab4a93160 100644 --- a/redisinsight/ui/src/components/navigation-menu/styles.module.scss +++ b/redisinsight/ui/src/components/navigation-menu/styles.module.scss @@ -53,6 +53,37 @@ $sideBarWidth: 60px; height: 20px; } } + + .navigationButtonWrapper { + position: relative; + + &:hover { + .betaLabel { + transform: translateX(-50%) translateY(-1px); + } + } + } + + .betaLabel { + position: absolute; + bottom: 8px; + left: 50%; + transform: translateX(-50%) translateY(0); + + font-size: 8px !important; + line-height: 12px !important; + background-color: var(--recommendationLiveBorderColor) !important; + border: 1px solid var(--triggerIconActiveColor) !important; + color: #FFF7EA !important; + border-radius: 2px !important; + + transition: transform 250ms ease-in-out; + pointer-events: none; + + :global(.euiBadge__content) { + min-height: 12px !important; + } + } } diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index 1e9398d3c8..0c4d53ea66 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -104,6 +104,8 @@ enum ApiEndpoints { RECOMMENDATIONS = 'recommendations', RECOMMENDATIONS_READ = 'recommendations/read', + TRIGGERED_FUNCTIONS_LIBRARIES = 'triggered-functions/libraries', + NOTIFICATIONS = 'notifications', NOTIFICATIONS_READ = 'notifications/read', diff --git a/redisinsight/ui/src/constants/pages.ts b/redisinsight/ui/src/constants/pages.ts index a35d2623e5..b681f2d2af 100644 --- a/redisinsight/ui/src/constants/pages.ts +++ b/redisinsight/ui/src/constants/pages.ts @@ -1,13 +1,11 @@ -import React from 'react' - export interface IRoute { - path: any; - component: React.ReactNode; - pageName?: PageNames; - exact?: boolean; - routes?: any; - protected?: boolean; - isAvailableWithoutAgreements?: boolean; + path: any + component: (routes: any) => JSX.Element | Element + pageName?: PageNames + exact?: boolean + routes?: any + protected?: boolean + isAvailableWithoutAgreements?: boolean } export enum PageNames { @@ -18,7 +16,10 @@ export enum PageNames { analytics = 'analytics', clusterDetails = 'cluster-details', databaseAnalysis = 'database-analysis', - settings = 'settings' + settings = 'settings', + triggeredFunctions = 'triggered-functions', + triggeredFunctionsLibraries = 'libraries', + triggeredFunctionsFunctions = 'functions', } const redisCloud = '/redis-cloud' @@ -43,4 +44,9 @@ export const Pages = { slowLog: (instanceId: string) => `/${instanceId}/${PageNames.analytics}/${PageNames.slowLog}`, clusterDetails: (instanceId: string) => `/${instanceId}/${PageNames.analytics}/${PageNames.clusterDetails}`, databaseAnalysis: (instanceId: string) => `/${instanceId}/${PageNames.analytics}/${PageNames.databaseAnalysis}`, + triggeredFunctions: (instanceId: string) => `/${instanceId}/${PageNames.triggeredFunctions}`, + triggeredFunctionsLibraries: (instanceId: string) => + `/${instanceId}/${PageNames.triggeredFunctions}/${PageNames.triggeredFunctionsLibraries}`, + triggeredFunctionsFunctions: (instanceId: string) => + `/${instanceId}/${PageNames.triggeredFunctions}/${PageNames.triggeredFunctionsFunctions}`, } diff --git a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx b/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx index 6248e142a9..be03a12a67 100644 --- a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx +++ b/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx @@ -30,6 +30,7 @@ export interface Props { containerClassName?: string turnOffAutoRefresh?: boolean onRefresh: (enableAutoRefresh: boolean) => void + onRefreshClicked: () => void onEnableAutoRefresh?: (enableAutoRefresh: boolean, refreshRate: string) => void onChangeAutoRefreshRate?: (enableAutoRefresh: boolean, refreshRate: string) => void } @@ -45,6 +46,7 @@ const AutoRefresh = ({ testid = '', turnOffAutoRefresh, onRefresh, + onRefreshClicked, onEnableAutoRefresh, onChangeAutoRefreshRate, }: Props) => { @@ -144,6 +146,11 @@ const AutoRefresh = ({ onRefresh(enableAutoRefresh) } + const handleRefreshClick = () => { + handleRefresh() + onRefreshClicked?.() + } + const onChangeEnableAutoRefresh = (value: boolean) => { setEnableAutoRefresh(value) @@ -170,7 +177,7 @@ const AutoRefresh = ({ { dispatch(setInitialAnalyticsSettings()) dispatch(setRedisearchInitialState()) dispatch(resetRecommendationsHighlighting()) + dispatch(setTriggeredFunctionsInitialState()) setTimeout(() => { dispatch(resetOutput()) }, 0) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx new file mode 100644 index 0000000000..6d293b9238 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx @@ -0,0 +1,59 @@ +import React, { useEffect, useState } from 'react' +import { useHistory } from 'react-router' +import { useParams } from 'react-router-dom' +import { useSelector } from 'react-redux' +import { Pages } from 'uiSrc/constants' +import InstanceHeader from 'uiSrc/components/instance-header' +import AnalyticsPageRouter from 'uiSrc/pages/analytics/AnalyticsPageRouter' + +import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' +import { appAnalyticsInfoSelector } from 'uiSrc/slices/app/info' +import styles from './styles.modules.scss' + +export interface Props { + routes: any[] +} + +const TriggeredFunctionsPage = ({ routes = [] }: Props) => { + const { identified: analyticsIdentified } = useSelector(appAnalyticsInfoSelector) + const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) + + const [isPageViewSent, setIsPageViewSent] = useState(false) + const { instanceId } = useParams<{ instanceId: string }>() + const history = useHistory() + + const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}` + setTitle(`${dbName} - Triggers & Functions`) + + useEffect(() => { + // TODO update routing + history.push(Pages.triggeredFunctionsLibraries(instanceId)) + }, []) + + useEffect(() => { + if (connectedInstanceName && !isPageViewSent && analyticsIdentified) { + sendPageView(instanceId) + } + }, [connectedInstanceName, isPageViewSent, analyticsIdentified]) + + const sendPageView = (instanceId: string) => { + sendPageViewTelemetry({ + name: TelemetryPageView.TRIGGERED_FUNCTIONS, + databaseId: instanceId + }) + setIsPageViewSent(true) + } + + return ( + <> + +
+ +
+ + ) +} + +export default TriggeredFunctionsPage diff --git a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPageRouter.tsx b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPageRouter.tsx new file mode 100644 index 0000000000..ce61918b20 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPageRouter.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { Switch } from 'react-router-dom' +import RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes' + +export interface Props { + routes: any[]; +} +const InstancePageRouter = ({ routes }: Props) => ( + + {routes.map((route, i) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + +) + +export default React.memo(InstancePageRouter) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/index.ts new file mode 100644 index 0000000000..7bb5554039 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/index.ts @@ -0,0 +1,3 @@ +import TriggeredFunctionsPage from './TriggeredFunctionsPage' + +export default TriggeredFunctionsPage diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx new file mode 100644 index 0000000000..37774c772b --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useState } from 'react' +import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui' + +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { + fetchTriggeredFunctionsLibrariesList, + triggeredFunctionsSelector +} from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { TriggeredFunctionsLibrary } from 'uiSrc/slices/interfaces/triggeredFunctions' +import NoLibrariesScreen from './components/NoLibrariesScreen' +import LibrariesList from './components/LibrariesList' + +import styles from './styles.module.scss' + +const LibrariesPage = () => { + const { lastRefresh, loading, libraries } = useSelector(triggeredFunctionsSelector) + const [items, setItems] = useState([]) + const [filterValue, setFilterValue] = useState('') + + const { instanceId } = useParams<{ instanceId: string }>() + const dispatch = useDispatch() + + useEffect(() => { + updateList() + }, []) + + useEffect(() => { + applyFiltering() + }, [filterValue, libraries]) + + const updateList = () => { + dispatch(fetchTriggeredFunctionsLibrariesList(instanceId)) + } + + const onChangeFiltering = (e: React.ChangeEvent) => { + setFilterValue(e.target.value.toLowerCase()) + } + + const applyFiltering = () => { + if (!filterValue) { + setItems(libraries || []) + return + } + + const itemsTemp = libraries?.filter((item: TriggeredFunctionsLibrary) => ( + item.name?.toLowerCase().indexOf(filterValue) !== -1 + || item.user?.toLowerCase().indexOf(filterValue) !== -1 + )) + + setItems(itemsTemp || []) + } + + if (!libraries) { + return <> + } + + return ( + + + + + {libraries?.length > 0 && ( + + )} + + + {}} + className={styles.addLibrary} + data-testid="btn-add-library" + > + + Library + + + + + + + {(libraries?.length > 0) && ( + + )} + {libraries?.length === 0 && ( + + )} + + + ) +} + +export default LibrariesPage diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx new file mode 100644 index 0000000000..bd9f6247fd --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx @@ -0,0 +1,141 @@ +import React, { useState } from 'react' +import { EuiBasicTableColumn, EuiInMemoryTable, EuiText, EuiToolTip, PropertySort } from '@elastic/eui' +import cx from 'classnames' + +import { Maybe, Nullable } from 'uiSrc/utils' +import AutoRefresh from 'uiSrc/pages/browser/components/auto-refresh' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { TriggeredFunctionsLibrary } from 'uiSrc/slices/interfaces/triggeredFunctions' +import styles from './styles.module.scss' + +export interface Props { + items: Nullable + loading: boolean + onRefresh: () => void + lastRefresh: Nullable +} + +const NoLibrariesMessage: React.ReactNode = (No Libraries found) + +const LibrariesList = (props: Props) => { + const { items, loading, onRefresh, lastRefresh } = props + const [sort, setSort] = useState>(undefined) + const [selectedRow, setSelectedRow] = useState>(null) + + const columns: EuiBasicTableColumn[] = [ + { + name: 'Library Name', + field: 'name', + sortable: true, + truncateText: true, + width: '25%', + render: (value: string) => ( + + <>{value} + + ) + }, + { + name: 'Username', + field: 'user', + sortable: true, + truncateText: true, + width: '25%', + render: (value: string) => ( + + <>{value} + + ) + }, + { + name: 'Pending', + field: 'pendingJobs', + align: 'right', + sortable: true, + width: '140x' + }, + { + name: 'Total Functions', + field: 'totalFunctions', + align: 'right', + width: '140px', + sortable: true, + }, + { + name: '', + field: 'actions', + width: '20%' + }, + ] + + const handleSelect = (item: TriggeredFunctionsLibrary) => { + setSelectedRow(item.name) + } + + const handleRefreshClicked = () => { + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_REFRESH_CLICKED, + }) + } + + const handleSorting = ({ sort }: any) => { + setSort(sort) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARIES_SORTED, + eventData: sort + }) + } + + const handleEnableAutoRefresh = (enableAutoRefresh: boolean, refreshRate: string) => { + sendEventTelemetry({ + event: enableAutoRefresh + ? TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_ENABLED + : TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED, + eventData: { + refreshRate + } + }) + } + + return ( +
+
+ Total: {items?.length || 0} + onRefresh?.()} + onRefreshClicked={handleRefreshClicked} + onEnableAutoRefresh={handleEnableAutoRefresh} + testid="refresh-libraries-btn" + /> +
+ ({ + onClick: () => handleSelect(row), + className: row.name === selectedRow ? 'selected' : '' + })} + message={NoLibrariesMessage} + onTableChange={handleSorting} + className={cx('inMemoryTableDefault', 'noBorders', styles.table)} + data-testid="libraries-list-table" + /> +
+ ) +} + +export default LibrariesList diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/index.ts new file mode 100644 index 0000000000..add9975fb0 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/index.ts @@ -0,0 +1,3 @@ +import LibrariesList from './LibrariesList' + +export default LibrariesList diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss new file mode 100644 index 0000000000..a590ecd8f3 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss @@ -0,0 +1,37 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/components/table/mixins"; +@import "@elastic/eui/src/global_styling/index"; + +.tableWrapper { + max-height: 100%; + + .header { + height: 42px; + background-color: var(--browserTableRowEven); + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 20px; + } +} + +.table { + @include euiScrollBar; + overflow: auto; + position: relative; + max-height: calc(100% - 42px); + + :global { + thead { + border-bottom: 1px solid var(--tableDarkestBorderColor); + } + + .euiTableHeaderCell { + background-color: var(--euiColorEmptyShade); + } + + .euiTableCellContent { + padding: 16px 18px !important; + } + } +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.tsx new file mode 100644 index 0000000000..36d779ee91 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +import { EuiIcon, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui' +import { ReactComponent as WelcomeIcon } from 'uiSrc/assets/img/icons/welcome.svg' + +import styles from './styles.module.scss' + +const NoLibrariesScreen = () => ( +
+
+ + + +

Triggers and Functions

+
+ + See an overview of triggers and functions uploaded, upload new libraries, and manage the list of existing ones. + + To start working with triggers and functions, click “+ Library” to upload a new library. +
+
+) + +export default NoLibrariesScreen diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/index.ts new file mode 100644 index 0000000000..f41b2770fb --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/index.ts @@ -0,0 +1,3 @@ +import NoLibrariesScreen from './NoLibrariesScreen' + +export default NoLibrariesScreen diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss new file mode 100644 index 0000000000..c1c56b3e68 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss @@ -0,0 +1,27 @@ +@import '@elastic/eui/src/global_styling/mixins/helpers'; +@import '@elastic/eui/src/global_styling/index'; + +.wrapper { + height: 100%; + width: 100%; + + @include euiScrollBar; + overflow: auto; + + padding: 40px; + display: flex; + justify-content: center; + align-items: center; +} + +.container { + text-align: center; + + max-width: 560px; + margin: 0 auto; +} + +.icon { + color: transparent; +} + diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/index.ts new file mode 100644 index 0000000000..99054ac363 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/index.ts @@ -0,0 +1,3 @@ +import LibrariesPage from './LibrariesPage' + +export default LibrariesPage diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss new file mode 100644 index 0000000000..cb80391a5c --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss @@ -0,0 +1,24 @@ +.topPanel { + margin: 16px 0; + min-height: 40px; + + :global(.euiFormControlLayout) { + max-width: 600px; + } + + .search { + &:global(.euiFieldSearch) { + position: relative; + border-top: none !important; + border-left: none !important; + border-right: none !important; + background-color: transparent !important; + } + } +} + + +.main { + background-color: var(--euiColorEmptyShade); + max-height: calc(100% - 72px); +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/index.ts new file mode 100644 index 0000000000..e756407f80 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/index.ts @@ -0,0 +1,5 @@ +import LibrariesPage from './Libraries' + +export { + LibrariesPage +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss b/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss new file mode 100644 index 0000000000..fb59aec8c5 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss @@ -0,0 +1,21 @@ +.main { + height: calc(100% - 70px); + padding: 16px 16px 0; + overflow: auto; + + border-top: 1px solid var(--euiColorLightShade); + + :global { + .euiTableRow { + border-left: 3px solid transparent; + + &:hover, &:focus { + background-color: var(--tableRowSelectedColor) !important; + } + &.selected { + background-color: var(--hoverInListColorDarken) !important; + border-left: 3px solid var(--euiColorPrimary); + } + } + } +} diff --git a/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts b/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts new file mode 100644 index 0000000000..a2eb3af0dc --- /dev/null +++ b/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts @@ -0,0 +1,22 @@ +import { Nullable } from 'uiSrc/utils' + +export interface TriggeredFunctionsFunctions { + flags: string[] + isAsync: boolean + name: string + type: string +} + +export interface TriggeredFunctionsLibrary { + name: string + user: string + pendingJobs: number + totalFunctions: number +} + +export interface StateTriggeredFunctions { + libraries: Nullable + loading: boolean, + lastRefresh: Nullable + error: string +} diff --git a/redisinsight/ui/src/slices/store.ts b/redisinsight/ui/src/slices/store.ts index 09d6f84e27..f820ca604a 100644 --- a/redisinsight/ui/src/slices/store.ts +++ b/redisinsight/ui/src/slices/store.ts @@ -39,6 +39,7 @@ import clusterDetailsReducer from './analytics/clusterDetails' import databaseAnalysisReducer from './analytics/dbAnalysis' import redisearchReducer from './browser/redisearch' import recommendationsReducer from './recommendations/recommendations' +import triggeredFunctionsReducer from './triggeredFunctions/triggeredFunctions' export const history = createBrowserHistory() @@ -97,6 +98,7 @@ export const rootReducer = combineReducers({ }), pubsub: pubSubReducer, recommendations: recommendationsReducer, + triggeredFunctions: triggeredFunctionsReducer }) const store = configureStore({ diff --git a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts new file mode 100644 index 0000000000..b92e3f43e6 --- /dev/null +++ b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts @@ -0,0 +1,78 @@ +import { createSlice } from '@reduxjs/toolkit' +import { AxiosError } from 'axios' +import { StateTriggeredFunctions } from 'uiSrc/slices/interfaces/triggeredFunctions' +import { AppDispatch, RootState } from 'uiSrc/slices/store' +import { apiService } from 'uiSrc/services' +import { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils' +import { ApiEndpoints } from 'uiSrc/constants' +import { addErrorNotification } from 'uiSrc/slices/app/notifications' + +export const initialState: StateTriggeredFunctions = { + libraries: null, + loading: false, + lastRefresh: null, + error: '', +} + +const triggeredFunctionsSlice = createSlice({ + name: 'triggeredFunctions', + initialState, + reducers: { + setTriggeredFunctionsInitialState: () => initialState, + getTriggeredFunctionsList: (state) => { + state.loading = true + state.error = '' + }, + getTriggeredFunctionsListSuccess: (state, { payload }) => { + state.loading = false + state.lastRefresh = Date.now() + state.libraries = payload + }, + getTriggeredFunctionsFailure: (state, { payload }) => { + state.loading = false + state.error = payload + } + } +}) + +export const { + setTriggeredFunctionsInitialState, + getTriggeredFunctionsList, + getTriggeredFunctionsListSuccess, + getTriggeredFunctionsFailure, +} = triggeredFunctionsSlice.actions + +export const triggeredFunctionsSelector = (state: RootState) => state.triggeredFunctions + +export default triggeredFunctionsSlice.reducer + +// Asynchronous thunk action +export function fetchTriggeredFunctionsLibrariesList( + instanceId: string, + onSuccessAction?: () => void, + onFailAction?: () => void, +) { + return async (dispatch: AppDispatch) => { + try { + dispatch(getTriggeredFunctionsList()) + + const { data, status } = await apiService.get( + getUrl( + instanceId, + ApiEndpoints.TRIGGERED_FUNCTIONS_LIBRARIES, + ) + ) + + if (isStatusSuccessful(status)) { + dispatch(getTriggeredFunctionsListSuccess(data)) + onSuccessAction?.() + } + } catch (_err) { + const error = _err as AxiosError + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + dispatch(getTriggeredFunctionsFailure(errorMessage)) + onFailAction?.() + } + } +} diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 8a224f2dab..c58c69f7ca 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -235,4 +235,9 @@ export enum TelemetryEvent { INSIGHTS_RECOMMENDATION_SHOW_HIDDEN = 'INSIGHTS_RECOMMENDATION_SHOW_HIDDEN', INSIGHTS_RECOMMENDATION_DATABASE_ANALYSIS_CLICKED = 'INSIGHTS_RECOMMENDATION_DATABASE_ANALYSIS_CLICKED', INSIGHTS_RECOMMENDATION_KEY_COPIED = 'INSIGHTS_RECOMMENDATION_KEY_COPIED', + + TRIGGERS_AND_FUNCTIONS_LIBRARIES_SORTED = 'TRIGGERS_AND_FUNCTIONS_LIBRARIES_SORTED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_REFRESH_CLICKED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_REFRESH_CLICKED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_ENABLED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_ENABLED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED' } diff --git a/redisinsight/ui/src/telemetry/pageViews.ts b/redisinsight/ui/src/telemetry/pageViews.ts index 6a60459319..72b2e9ac39 100644 --- a/redisinsight/ui/src/telemetry/pageViews.ts +++ b/redisinsight/ui/src/telemetry/pageViews.ts @@ -7,5 +7,6 @@ export enum TelemetryPageView { SLOWLOG_PAGE = 'Slow Log', CLUSTER_DETAILS_PAGE = 'Overview', PUBSUB_PAGE = 'Pub/Sub', - DATABASE_ANALYSIS = 'Database Analysis' + DATABASE_ANALYSIS = 'Database Analysis', + TRIGGERED_FUNCTIONS = 'Triggers & Functions' } diff --git a/redisinsight/ui/src/utils/test-utils.tsx b/redisinsight/ui/src/utils/test-utils.tsx index 719f1b3e1a..05008e709f 100644 --- a/redisinsight/ui/src/utils/test-utils.tsx +++ b/redisinsight/ui/src/utils/test-utils.tsx @@ -46,6 +46,7 @@ import { initialState as initialStateDbAnalysis } from 'uiSrc/slices/analytics/d import { initialState as initialStatePubSub } from 'uiSrc/slices/pubsub/pubsub' import { initialState as initialStateRedisearch } from 'uiSrc/slices/browser/redisearch' import { initialState as initialStateRecommendations } from 'uiSrc/slices/recommendations/recommendations' +import { initialState as initialStateTriggeredFunctions } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService' import { apiService } from 'uiSrc/services' @@ -112,6 +113,7 @@ const initialStateDefault: RootState = { }, recommendations: cloneDeep(initialStateRecommendations), pubsub: cloneDeep(initialStatePubSub), + triggeredFunctions: cloneDeep(initialStateTriggeredFunctions) } // mocked store From e0f1c8ea6e2be93176803650fbc332ade01ed15b Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Mon, 12 Jun 2023 14:20:43 +0300 Subject: [PATCH 009/106] Be/feature ri 4592 triggered functions (#2185) * RI-4592 - add triggered functions endpoint --- redisinsight/api/src/__mocks__/index.ts | 1 + .../api/src/__mocks__/triggered-functions.ts | 36 +++ redisinsight/api/src/app.module.ts | 2 + redisinsight/api/src/app.routes.ts | 5 + .../modules/triggered-functions/dto/index.ts | 1 + .../triggered-functions/dto/library.dto.ts | 13 + .../triggered-functions/models/function.ts | 156 ++++++++++++ .../triggered-functions/models/index.ts | 4 + .../triggered-functions/models/library.ts | 80 +++++++ .../models/short-function.ts | 6 + .../models/short-library.ts | 6 + .../triggered-functions.controller.ts | 74 ++++++ .../triggered-functions.module.ts | 11 + .../triggered-functions.service.spec.ts | 222 ++++++++++++++++++ .../triggered-functions.service.ts | 109 +++++++++ .../triggered-functions/utils/index.ts | 1 + .../utils/triggered-functions.util.spec.ts | 172 ++++++++++++++ .../utils/triggered-functions.util.ts | 103 ++++++++ 18 files changed, 1002 insertions(+) create mode 100644 redisinsight/api/src/__mocks__/triggered-functions.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/dto/index.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/dto/library.dto.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/models/function.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/models/index.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/models/library.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/models/short-function.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/models/short-library.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/triggered-functions.module.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/utils/index.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.spec.ts create mode 100644 redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts diff --git a/redisinsight/api/src/__mocks__/index.ts b/redisinsight/api/src/__mocks__/index.ts index 4298564992..a39ae9d89c 100644 --- a/redisinsight/api/src/__mocks__/index.ts +++ b/redisinsight/api/src/__mocks__/index.ts @@ -23,3 +23,4 @@ export * from './ssh'; export * from './browser-history'; export * from './database-recommendation'; export * from './feature'; +export * from './triggered-functions'; diff --git a/redisinsight/api/src/__mocks__/triggered-functions.ts b/redisinsight/api/src/__mocks__/triggered-functions.ts new file mode 100644 index 0000000000..e6346fc087 --- /dev/null +++ b/redisinsight/api/src/__mocks__/triggered-functions.ts @@ -0,0 +1,36 @@ +export const mockCommonLibraryReply = [ + 'api_version', '1.0', + 'engine', 'js', + 'configuration', null, + 'name', 'libraryName', + 'pending_jobs', 0, + 'user', 'default', +]; + +export const mockSimpleLibraryReply = [ + 'api_version', '1.0', + 'engine', 'js', + 'configuration', null, + 'functions', ['foo'], + 'keyspace_triggers', ['keyspace'], + 'cluster_functions', ['cluster'], + 'stream_triggers', ['stream'], + 'name', 'libraryName', + 'pending_jobs', 0, + 'user', 'default', +]; + +export const mockVerboseLibraryReply = [ + 'api_version', '1.0', + 'engine', 'js', + 'configuration', null, + 'name', 'libraryName', + 'pending_jobs', 0, + 'user', 'default', + 'functions', [['name', 'function', 'description', 'description', 'is_async', 1, 'flags', ['flag1']]], + 'keyspace_triggers', [], + 'cluster_functions', ['foo', 'bar'], + 'stream_triggers', [[ + 'name', 'stream', 'description', 'description', 'prefix', 'prefix', 'trim', 0, 'window', 1, 'streams', [['key', 'value']], + ]], +]; diff --git a/redisinsight/api/src/app.module.ts b/redisinsight/api/src/app.module.ts index 4a4fd36a07..fb6c427fae 100644 --- a/redisinsight/api/src/app.module.ts +++ b/redisinsight/api/src/app.module.ts @@ -15,6 +15,7 @@ import { NotificationModule } from 'src/modules/notification/notification.module import { BulkActionsModule } from 'src/modules/bulk-actions/bulk-actions.module'; import { ClusterMonitorModule } from 'src/modules/cluster-monitor/cluster-monitor.module'; import { DatabaseAnalysisModule } from 'src/modules/database-analysis/database-analysis.module'; +import { TriggeredFunctionsModule } from 'src/modules/triggered-functions/triggered-functions.module'; import { ServerModule } from 'src/modules/server/server.module'; import { LocalDatabaseModule } from 'src/local-database.module'; import { CoreModule } from 'src/core.module'; @@ -57,6 +58,7 @@ const PATH_CONFIG = config.get('dir_path'); CustomTutorialModule.register(), DatabaseAnalysisModule, DatabaseImportModule, + TriggeredFunctionsModule, ...(SERVER_CONFIG.staticContent ? [ ServeStaticModule.forRoot({ diff --git a/redisinsight/api/src/app.routes.ts b/redisinsight/api/src/app.routes.ts index 4d808cf646..7e6901972d 100644 --- a/redisinsight/api/src/app.routes.ts +++ b/redisinsight/api/src/app.routes.ts @@ -6,6 +6,7 @@ import { SlowLogModule } from 'src/modules/slow-log/slow-log.module'; import { PubSubModule } from 'src/modules/pub-sub/pub-sub.module'; import { ClusterMonitorModule } from 'src/modules/cluster-monitor/cluster-monitor.module'; import { DatabaseAnalysisModule } from 'src/modules/database-analysis/database-analysis.module'; +import { TriggeredFunctionsModule } from 'src/modules/triggered-functions/triggered-functions.module'; import { BulkActionsModule } from 'src/modules/bulk-actions/bulk-actions.module'; import { DatabaseRecommendationModule } from 'src/modules/database-recommendation/database-recommendation.module'; @@ -49,6 +50,10 @@ export const routes: Routes = [ path: '/:dbInstance', module: DatabaseRecommendationModule, }, + { + path: '/:dbInstance', + module: TriggeredFunctionsModule, + }, ], }, ]; diff --git a/redisinsight/api/src/modules/triggered-functions/dto/index.ts b/redisinsight/api/src/modules/triggered-functions/dto/index.ts new file mode 100644 index 0000000000..1f35351615 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/dto/index.ts @@ -0,0 +1 @@ +export * from './library.dto'; diff --git a/redisinsight/api/src/modules/triggered-functions/dto/library.dto.ts b/redisinsight/api/src/modules/triggered-functions/dto/library.dto.ts new file mode 100644 index 0000000000..ee276725c4 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/dto/library.dto.ts @@ -0,0 +1,13 @@ +import { + IsDefined, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class LibraryDto { + @ApiProperty({ + description: 'Library Name', + type: String, + }) + @IsDefined() + libraryName: string; +} diff --git a/redisinsight/api/src/modules/triggered-functions/models/function.ts b/redisinsight/api/src/modules/triggered-functions/models/function.ts new file mode 100644 index 0000000000..b275072e8c --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/models/function.ts @@ -0,0 +1,156 @@ +import { Expose, Transform } from 'class-transformer'; +import { + IsArray, IsEnum, IsOptional, IsString, IsBoolean, IsNumber, +} from 'class-validator'; +import { isNumber } from 'lodash'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum FunctionType { + Function = 'functions', + ClusterFunction = 'cluster_functions', + KeyspaceTrigger = 'keyspace_triggers', + StreamTrigger = 'stream_triggers', +} + +export class Function { + @ApiProperty({ + description: 'Function type', + enum: FunctionType, + }) + @IsEnum(FunctionType) + @Expose() + type: FunctionType; + + @ApiProperty({ + description: 'Function name', + type: String, + example: 'name', + }) + @IsString() + @Expose() + name: string; + + @ApiPropertyOptional({ + description: 'Library name', + type: String, + example: 'lib', + }) + @IsString() + @Expose() + library?: string; + + @ApiPropertyOptional({ + description: 'Total succeed function', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + success?: number; + + @ApiPropertyOptional({ + description: 'Total failed function', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + fail?: number; + + @ApiPropertyOptional({ + description: 'Total trigger function', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + total?: number; + + @ApiPropertyOptional({ + description: 'Function flags', + type: String, + isArray: true, + }) + @IsArray() + @Expose() + flags?: string[]; + + @ApiPropertyOptional({ + description: 'Is function is async', + type: Boolean, + example: false, + }) + @IsBoolean() + @IsOptional() + @Expose() + @Transform((val) => (isNumber(val) ? val === 1 : undefined)) + isAsync?: boolean; + + @ApiPropertyOptional({ + description: 'Function description', + type: String, + example: 'some description', + }) + @IsString() + @IsOptional() + @Expose() + description?: string; + + @ApiPropertyOptional({ + description: 'Last execution error', + type: String, + example: 'error', + }) + @IsString() + @IsOptional() + @Expose() + lastError?: string; + + @ApiPropertyOptional({ + description: 'Last function execution time', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + lastExecutionTime?: number; + + @ApiPropertyOptional({ + description: 'Total execution time', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + totalExecutionTime?: number; + + @ApiPropertyOptional({ + description: 'Stream prefix', + type: String, + example: 'stream', + }) + @IsString() + @IsOptional() + @Expose() + prefix?: string; + + @ApiPropertyOptional({ + description: 'Whether or not to trim the stream', + type: Boolean, + example: true, + }) + @IsBoolean() + @IsOptional() + @Transform((val) => (isNumber(val) ? val === 1 : undefined)) + @Expose() + trim?: boolean; + + @ApiPropertyOptional({ + description: 'How many elements can be processed simultaneously', + type: Number, + example: 1, + }) + @IsNumber() + @Expose() + window?: number; +} diff --git a/redisinsight/api/src/modules/triggered-functions/models/index.ts b/redisinsight/api/src/modules/triggered-functions/models/index.ts new file mode 100644 index 0000000000..3120e8a147 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/models/index.ts @@ -0,0 +1,4 @@ +export * from './function'; +export * from './library'; +export * from './short-function'; +export * from './short-library'; diff --git a/redisinsight/api/src/modules/triggered-functions/models/library.ts b/redisinsight/api/src/modules/triggered-functions/models/library.ts new file mode 100644 index 0000000000..f945390992 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/models/library.ts @@ -0,0 +1,80 @@ +import { Expose, Type } from 'class-transformer'; +import { + IsArray, IsString, IsNumber, +} from 'class-validator'; +import { ShortFunction } from 'src/modules/triggered-functions/models'; +import { ApiProperty } from '@nestjs/swagger'; + +export class Library { + @ApiProperty({ + description: 'Library name', + type: String, + example: 'name', + }) + @IsString() + @Expose() + name: string; + + @ApiProperty({ + description: 'Library apy version', + type: String, + example: '1.0', + }) + @IsString() + @Expose() + apiVersion: string; + + @ApiProperty({ + description: 'User name', + type: String, + example: 'default', + }) + @IsString() + @Expose() + user: string; + + @ApiProperty({ + description: 'Total of pending jobs', + type: Number, + example: 0, + }) + @IsNumber() + @Expose() + pendingJobs: number; + + @ApiProperty({ + description: 'Library configuration', + type: String, + }) + @IsString() + @Expose() + configuration: string; + + @ApiProperty({ + description: 'Library code', + type: String, + example: 0, + }) + @IsString() + @Expose() + code: string; + + @ApiProperty({ + description: 'Array of functions with name, type fields', + isArray: true, + type: ShortFunction, + }) + @IsArray() + @Type(() => ShortFunction) + @Expose() + functions: ShortFunction[]; + + @ApiProperty({ + description: 'Total number of functions', + type: Number, + example: 0, + }) + @IsNumber() + @Expose() + totalFunctions: number; +} diff --git a/redisinsight/api/src/modules/triggered-functions/models/short-function.ts b/redisinsight/api/src/modules/triggered-functions/models/short-function.ts new file mode 100644 index 0000000000..b589238ce5 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/models/short-function.ts @@ -0,0 +1,6 @@ +import { PartialType, PickType } from '@nestjs/swagger'; +import { Function } from 'src/modules/triggered-functions/models'; + +export class ShortFunction extends PartialType( + PickType(Function, ['name', 'type'] as const), +) {} diff --git a/redisinsight/api/src/modules/triggered-functions/models/short-library.ts b/redisinsight/api/src/modules/triggered-functions/models/short-library.ts new file mode 100644 index 0000000000..3475230a3d --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/models/short-library.ts @@ -0,0 +1,6 @@ +import { PartialType, PickType } from '@nestjs/swagger'; +import { Library } from 'src/modules/triggered-functions/models/library'; + +export class ShortLibrary extends PartialType( + PickType(Library, ['name', 'user', 'totalFunctions', 'pendingJobs'] as const), +) {} diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts new file mode 100644 index 0000000000..635898f7d5 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts @@ -0,0 +1,74 @@ +import { + Get, + Post, + Controller, UsePipes, ValidationPipe, Body, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; +import { TriggeredFunctionsService } from 'src/modules/triggered-functions/triggered-functions.service'; +import { ShortLibrary, Library, Function } from 'src/modules/triggered-functions/models'; +import { LibraryDto } from 'src/modules/triggered-functions/dto'; +import { ClientMetadata } from 'src/common/models'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; + +@ApiTags('Triggered Functions') +@Controller('triggered-functions') +@UsePipes(new ValidationPipe()) +export class TriggeredFunctionsController { + constructor(private service: TriggeredFunctionsService) {} + + @Get('/libraries') + @ApiRedisInstanceOperation({ + description: 'Returns short libraries information', + statusCode: 200, + responses: [ + { + status: 200, + description: 'Returns libraries', + type: ShortLibrary, + }, + ], + }) + async libraryList( + @BrowserClientMetadata() clientMetadata: ClientMetadata, + ): Promise { + return this.service.libraryList(clientMetadata); + } + + @Post('/get-library') + @ApiRedisInstanceOperation({ + description: 'Returns library details', + statusCode: 200, + responses: [ + { + status: 200, + description: 'Returns library information', + type: Library, + }, + ], + }) + async details( + @BrowserClientMetadata() clientMetadata: ClientMetadata, + @Body() dto: LibraryDto, + ): Promise { + return this.service.details(clientMetadata, dto.libraryName); + } + + @Get('/functions') + @ApiRedisInstanceOperation({ + description: 'Returns function information', + statusCode: 200, + responses: [ + { + status: 200, + description: 'Returns all functions', + type: Function, + }, + ], + }) + async functionsList( + @BrowserClientMetadata() clientMetadata: ClientMetadata, + ): Promise { + return this.service.functionsList(clientMetadata); + } +} diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.module.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.module.ts new file mode 100644 index 0000000000..92cefc918b --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TriggeredFunctionsController } from 'src/modules/triggered-functions/triggered-functions.controller'; +import { TriggeredFunctionsService } from 'src/modules/triggered-functions/triggered-functions.service'; + +@Module({ + controllers: [TriggeredFunctionsController], + providers: [ + TriggeredFunctionsService, + ], +}) +export class TriggeredFunctionsModule {} diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts new file mode 100644 index 0000000000..7103a4ddd9 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts @@ -0,0 +1,222 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { plainToClass } from 'class-transformer'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { + mockClientMetadata, + mockDatabaseConnectionService, + mockIORedisClient, + mockVerboseLibraryReply, + mockSimpleLibraryReply, + MockType, +} from 'src/__mocks__'; +import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { TriggeredFunctionsService } from 'src/modules/triggered-functions/triggered-functions.service'; +import { FunctionType, ShortFunction, Function } from './models'; + +const mockLibrariesReply = [ + mockSimpleLibraryReply, + ['api_version', '1.0', + 'cluster_functions', [], + 'configuration', null, + 'engine', 'js', + 'functions', ['foo1'], + 'keyspace_triggers', [], + 'name', 'library2', + 'pending_jobs', 1, + 'stream_triggers', [], + 'user', 'default'], +]; + +const mockTFunctionsVerboseReply = [[ + 'api_version', '1.0', + 'code', 'some code', + 'configuration', '{ name: "value" }', + 'functions', ['foo', 'bar'], + 'stream_triggers', ['stream_foo'], + 'keyspace_triggers', ['keyspace_foo'], + 'cluster_functions', ['cluster_foo'], + 'pending_jobs', 1, + 'name', 'library', + 'user', 'user', +]]; + +const mockLibraryName = 'name'; + +describe('TriggeredFunctionsService', () => { + let service: TriggeredFunctionsService; + let databaseConnectionService: MockType; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TriggeredFunctionsService, + { + provide: DatabaseConnectionService, + useFactory: mockDatabaseConnectionService, + }, + ], + }).compile(); + + service = module.get(TriggeredFunctionsService); + databaseConnectionService = module.get(DatabaseConnectionService); + }); + + describe('functionsList', () => { + it('should return list of functions', async () => { + mockIORedisClient.sendCommand.mockResolvedValueOnce([mockVerboseLibraryReply]); + const list = await service.functionsList(mockClientMetadata); + + expect(list).toEqual([ + plainToClass(Function, { + name: 'function', + type: FunctionType.Function, + description: 'description', + flags: ['flag1'], + isAsync: 1, + library: 'libraryName', + }), + plainToClass(Function, { + name: 'foo', + type: FunctionType.ClusterFunction, + library: 'libraryName', + }), + plainToClass(Function, { + name: 'bar', + type: FunctionType.ClusterFunction, + library: 'libraryName', + }), + plainToClass(Function, { + description: 'description', + library: 'libraryName', + name: 'stream', + prefix: 'prefix', + trim: 0, + window: 1, + type: FunctionType.StreamTrigger, + }), + ]); + }); + + it('Should throw Error when error during creating a client in functionsList', async () => { + databaseConnectionService.createClient.mockRejectedValueOnce(new Error()); + await expect(service.functionsList(mockClientMetadata)).rejects.toThrow(Error); + }); + + it('should handle acl error NOPERM', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new Error('NOPERM')); + await service.functionsList(mockClientMetadata); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(ForbiddenException); + } + }); + + it('should handle HTTP error', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new NotFoundException('Not Found')); + await service.functionsList(mockClientMetadata); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); + }); + + describe('details', () => { + it('should return list of libraries', async () => { + mockIORedisClient.sendCommand.mockResolvedValueOnce(mockTFunctionsVerboseReply); + const library = await service.details(mockClientMetadata, mockLibraryName); + + expect(library).toEqual({ + name: 'library', + user: 'user', + pendingJobs: 1, + apiVersion: '1.0', + configuration: '{ name: "value" }', + code: 'some code', + functions: [ + plainToClass(ShortFunction, { name: 'foo', type: 'functions' }), + plainToClass(ShortFunction, { name: 'bar', type: 'functions' }), + plainToClass(ShortFunction, { name: 'cluster_foo', type: 'cluster_functions' }), + plainToClass(ShortFunction, { name: 'keyspace_foo', type: 'keyspace_triggers' }), + plainToClass(ShortFunction, { name: 'stream_foo', type: 'stream_triggers' }), + ], + }); + }); + + it('Should throw Error when error during creating a client in details', async () => { + databaseConnectionService.createClient.mockRejectedValueOnce(new Error()); + await expect(service.details(mockClientMetadata, mockLibraryName)).rejects.toThrow(Error); + }); + + it('should handle acl error', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new Error('NOPERM')); + await service.details(mockClientMetadata, mockLibraryName); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(ForbiddenException); + } + }); + + it('should handle HTTP error', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new NotFoundException('Not Found')); + await service.details(mockClientMetadata, mockLibraryName); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); + }); + + describe('libraryList', () => { + it('should return list of libraries', async () => { + mockIORedisClient.sendCommand.mockResolvedValueOnce(mockLibrariesReply); + const list = await service.libraryList(mockClientMetadata); + + expect(list).toEqual([ + { + name: 'libraryName', + user: 'default', + pendingJobs: 0, + totalFunctions: 4, + }, + { + name: 'library2', + user: 'default', + pendingJobs: 1, + totalFunctions: 1, + }, + ]); + }); + + it('Should throw Error when error during creating a client in libraryList', async () => { + databaseConnectionService.createClient.mockRejectedValueOnce(new Error()); + await expect(service.libraryList(mockClientMetadata)).rejects.toThrow(Error); + }); + + it('should handle acl error', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new Error('NOPERM')); + await service.libraryList(mockClientMetadata); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(ForbiddenException); + } + }); + + it('should handle HTTP error', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new NotFoundException('Not Found')); + await service.libraryList(mockClientMetadata); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); + }); +}); diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts new file mode 100644 index 0000000000..4caf5adb79 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts @@ -0,0 +1,109 @@ +import { Command } from 'ioredis'; +import { HttpException, Injectable, Logger } from '@nestjs/common'; +import { catchAclError } from 'src/utils'; +import { concat } from 'lodash'; +import { plainToClass } from 'class-transformer'; +import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { ShortLibrary, Library, Function } from 'src/modules/triggered-functions/models'; +import { + getLibraryInformation, getShortLibraryInformation, getLibraryFunctions, +} from 'src/modules/triggered-functions/utils'; +import { ClientMetadata } from 'src/common/models'; + +@Injectable() +export class TriggeredFunctionsService { + private logger = new Logger('TriggeredFunctionsService'); + + constructor( + private readonly databaseConnectionService: DatabaseConnectionService, + ) {} + + /** + * Get library list for particular database with name, user, totalFunctions, pendingJobs fields only + * @param clientMetadata + */ + public async libraryList( + clientMetadata: ClientMetadata, + ): Promise { + let client; + try { + client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); + const reply = await client.sendCommand( + new Command('TFUNCTION', ['LIST'], { replyEncoding: 'utf8' }), + ); + const libraries = reply.map((lib: string[]) => getShortLibraryInformation(lib)); + return libraries.map((lib) => plainToClass( + ShortLibrary, + lib, + )); + } catch (e) { + this.logger.error('Unable to get database libraries', e); + + if (e instanceof HttpException) { + throw e; + } + + throw catchAclError(e); + } + } + + /** + * Get library details + * @param clientMetadata + * @param name + */ + async details( + clientMetadata: ClientMetadata, + name: string, + ): Promise { + let client; + try { + client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); + const reply = await client.sendCommand( + new Command('TFUNCTION', ['LIST', 'WITHCODE', 'LIBRARY', name], { replyEncoding: 'utf8' }), + ); + const library = getLibraryInformation(reply[0]); + return plainToClass( + Library, + library, + ); + } catch (e) { + this.logger.error('Unable to get library details', e); + + if (e instanceof HttpException) { + throw e; + } + + throw catchAclError(e); + } + } + + /** + * Get all triggered functions + * @param clientMetadata + */ + async functionsList( + clientMetadata: ClientMetadata, + ): Promise { + let client; + try { + client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); + const reply = await client.sendCommand( + new Command('TFUNCTION', ['LIST', 'vvv'], { replyEncoding: 'utf8' }), + ); + const functions = reply.reduce((prev, cur) => concat(prev, getLibraryFunctions(cur)), []); + return functions.map((func) => plainToClass( + Function, + func, + )); + } catch (e) { + this.logger.error('Unable to get all triggered functions', e); + + if (e instanceof HttpException) { + throw e; + } + + throw catchAclError(e); + } + } +} diff --git a/redisinsight/api/src/modules/triggered-functions/utils/index.ts b/redisinsight/api/src/modules/triggered-functions/utils/index.ts new file mode 100644 index 0000000000..e7237f85ab --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/utils/index.ts @@ -0,0 +1 @@ +export * from './triggered-functions.util'; diff --git a/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.spec.ts b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.spec.ts new file mode 100644 index 0000000000..5b6d47edb6 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.spec.ts @@ -0,0 +1,172 @@ +import { + mockSimpleLibraryReply, + mockCommonLibraryReply, +} from 'src/__mocks__'; +import { FunctionType } from 'src/modules/triggered-functions/models'; +import { + getLibraryFunctions, + getShortLibraryInformation, + getLibraryInformation, +} from './triggered-functions.util'; + +const getLibraryFunctionsTests = [ + { + input: [ + ...mockCommonLibraryReply, + 'functions', [], + 'keyspace_triggers', [], + 'cluster_functions', [], + 'stream_triggers', [], + ], + expected: [], + }, + { + input: [ + ...mockCommonLibraryReply, + 'functions', [], + ], + expected: [], + }, + { + input: [ + ...mockCommonLibraryReply, + 'functions', [['name', 'function', 'description', 'description', 'is_async', 1, 'flags', ['flag1']]], + 'keyspace_triggers', [], + 'cluster_functions', ['foo', 'bar'], + 'stream_triggers', [[ + 'name', 'stream', 'description', 'description', 'prefix', 'prefix', 'trim', 0, 'window', 1, 'streams', [['key', 'value']], + ]], + ], + expected: [ + { + name: 'function', + description: 'description', + isAsync: 1, + flags: ['flag1'], + type: FunctionType.Function, + library: 'libraryName', + }, + { + name: 'foo', + library: 'libraryName', + type: FunctionType.ClusterFunction, + }, + { + name: 'bar', + library: 'libraryName', + type: FunctionType.ClusterFunction, + }, + { + name: 'stream', + description: 'description', + type: FunctionType.StreamTrigger, + window: 1, + trim: 0, + prefix: 'prefix', + library: 'libraryName', + }, + ], + }, +]; + +describe('getLibraryFunctions', () => { + it.each(getLibraryFunctionsTests)('%j', ({ input, expected }) => { + expect(getLibraryFunctions(input as string[])).toEqual(expected); + }); +}); + +const getShortLibraryInformationTests = [ + { + input: mockSimpleLibraryReply, + expected: { + name: 'libraryName', + pendingJobs: 0, + user: 'default', + totalFunctions: 4, + }, + }, + { + input: [ + ...mockCommonLibraryReply, + 'functions', [], + 'keyspace_triggers', [], + 'cluster_functions', [], + 'stream_triggers', [], + ], + expected: { + name: 'libraryName', + pendingJobs: 0, + user: 'default', + totalFunctions: 0, + }, + }, + { + input: [ + ...mockCommonLibraryReply, + 'functions', ['foo'], + ], + expected: { + name: 'libraryName', + pendingJobs: 0, + user: 'default', + totalFunctions: 1, + }, + }, +]; + +describe('getShortLibraryInformation', () => { + test.each(getShortLibraryInformationTests)('%j', ({ input, expected }) => { + expect(getShortLibraryInformation(input as string[])).toEqual(expected); + }); +}); + +const getLibraryInformationTests = [ + { + input: [ + ...mockSimpleLibraryReply, + 'code', 'some code', + ], + expected: { + name: 'libraryName', + pendingJobs: 0, + user: 'default', + apiVersion: '1.0', + code: 'some code', + configuration: null, + functions: [ + { name: 'foo', type: FunctionType.Function }, + { name: 'cluster', type: FunctionType.ClusterFunction }, + { name: 'keyspace', type: FunctionType.KeyspaceTrigger }, + { name: 'stream', type: FunctionType.StreamTrigger }, + ], + }, + }, + { + input: [ + ...mockCommonLibraryReply, + 'functions', ['foo'], + 'cluster_functions', ['cluster'], + 'keyspace_triggers', ['keyspace'], + 'code', 'some code', + ], + expected: { + name: 'libraryName', + pendingJobs: 0, + user: 'default', + apiVersion: '1.0', + code: 'some code', + configuration: null, + functions: [ + { name: 'foo', type: FunctionType.Function }, + { name: 'cluster', type: FunctionType.ClusterFunction }, + { name: 'keyspace', type: FunctionType.KeyspaceTrigger }, + ], + }, + }, +]; + +describe('getLibraryInformation', () => { + it.each(getLibraryInformationTests)('%j', ({ input, expected }) => { + expect(getLibraryInformation(input as string[])).toEqual(expected); + }); +}); diff --git a/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts new file mode 100644 index 0000000000..5adedf8992 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/utils/triggered-functions.util.ts @@ -0,0 +1,103 @@ +import { concat } from 'lodash'; +import { convertStringsArrayToObject } from 'src/utils'; +import { FunctionType, Function } from 'src/modules/triggered-functions/models'; + +/** + * Get function details +*/ +const getFunctionDetails = ( + functions: string[][] | string[], + type: FunctionType, + libName: string, +): Function[] => functions.map((reply: string | string[]) => { + if (type === FunctionType.ClusterFunction) { + return ({ + name: reply as string, + type, + library: libName, + }); + } + + const func = convertStringsArrayToObject(reply as string[]); + + return ({ + name: func.name, + success: func.num_success, + fail: func.num_failed, + total: func.num_trigger, + isAsync: func.is_async, + flags: func.flags, + lastError: func.last_error, + lastExecutionTime: func.last_execution_time, + totalExecutionTime: func.total_execution_time, + prefix: func.prefix, + trim: func.trim, + window: func.window, + description: func.description, + type, + library: libName, + }); +}); + +const getFunctionName = ( + functions: string[], + type: FunctionType, +): Function[] => functions.map((reply) => ({ + name: reply as string, + type, +})); + +/** + * Get all function names +*/ +const getFunctionNames = ( + lib, +): Partial[] => { + const functionGroups = Object.values(FunctionType).map((type) => getFunctionName(lib[type] || [], type)); + return concat(...functionGroups); +}; + +/** + * Get all functions +*/ +const collectFunctions = (lib) => { + const functionGroups = Object.values(FunctionType).map((type) => getFunctionDetails(lib[type] || [], type, lib.name)); + return concat(...functionGroups); +}; + +/** + * Get functions count +*/ +const getTotalFunctions = (lib: { [key: string]: Function[] }) => ( + Object.values(FunctionType).reduce((prev, cur) => prev + (lib[cur]?.length || 0), 0) +); + +/** + * Get library information +*/ +export const getLibraryInformation = (lib: string[]) => { + const library = convertStringsArrayToObject(lib); + const functions = getFunctionNames(library); + return ({ + name: library.name, + apiVersion: library.api_version, + user: library.user, + pendingJobs: library.pending_jobs, + configuration: library.configuration, + code: library.code, + functions, + }); +}; + +export const getShortLibraryInformation = (lib: string[]) => { + const library = convertStringsArrayToObject(lib); + const totalFunctions = getTotalFunctions(library); + return ({ + name: library.name, + user: library.user, + pendingJobs: library.pending_jobs, + totalFunctions, + }); +}; + +export const getLibraryFunctions = (lib: string[]) => collectFunctions(convertStringsArrayToObject(lib)); From 6091d990ea6595807d6005491deff1b4937ce390 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 12 Jun 2023 13:21:38 +0200 Subject: [PATCH 010/106] #RI-4593 - add tests for libraries list --- .../TriggeredFunctionsPage.spec.tsx | 33 ++++ .../TriggeredFunctionsPageRouter.spec.tsx | 17 ++ .../pages/Libraries/LibrariesPage.spec.tsx | 106 ++++++++++++ .../LibrariesList/LibrariesList.spec.tsx | 84 +++++++++ .../LibrariesList/LibrariesList.tsx | 3 +- .../NoLibrariesScreen.spec.tsx | 10 ++ .../triggeredFunctions.spec.ts | 159 ++++++++++++++++++ 7 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPageRouter.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx diff --git a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.spec.tsx new file mode 100644 index 0000000000..350bdff215 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.spec.tsx @@ -0,0 +1,33 @@ +import React from 'react' + +import { BrowserRouter } from 'react-router-dom' +import { instance, mock } from 'ts-mockito' +import { cloneDeep } from 'lodash' +import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils' +import TriggeredFunctionsPage, { Props } from './TriggeredFunctionsPage' + +const mockedProps = mock() + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +/** + * AnalyticsPage tests + * + * @group component + */ +describe('TriggeredFunctionsPage', () => { + it('should render', () => { + expect( + render( + + + + ) + ).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPageRouter.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPageRouter.spec.tsx new file mode 100644 index 0000000000..a990ef6cae --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPageRouter.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { render } from 'uiSrc/utils/test-utils' +import TriggeredFunctionsPageRouter from './TriggeredFunctionsPageRouter' + +const mockedRoutes = [ + { + path: '/page', + }, +] + +describe('TriggeredFunctionsPageRouter', () => { + it('should render', () => { + expect( + render(, { withRouter: true }) + ).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx new file mode 100644 index 0000000000..45ab1cae36 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx @@ -0,0 +1,106 @@ +import React from 'react' +import { cloneDeep } from 'lodash' +import { cleanup, mockedStore, render, screen, fireEvent } from 'uiSrc/utils/test-utils' + +import { getTriggeredFunctionsList, triggeredFunctionsSelector } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import LibrariesPage from './LibrariesPage' + +jest.mock('uiSrc/slices/triggeredFunctions/triggeredFunctions', () => ({ + ...jest.requireActual('uiSrc/slices/triggeredFunctions/triggeredFunctions'), + triggeredFunctionsSelector: jest.fn().mockReturnValue({ + loading: false, + libraries: null + }), +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +const mockedLibraries = [ + { + name: 'lib1', + user: 'user1', + totalFunctions: 2, + pendingJobs: 1 + }, + { + name: 'lib2', + user: 'user1', + totalFunctions: 2, + pendingJobs: 1 + }, + { + name: 'lib3', + user: 'user2', + totalFunctions: 2, + pendingJobs: 1 + } +] + +describe('LibrariesPage', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should fetch list of libraries', () => { + render() + + const expectedActions = [getTriggeredFunctionsList()] + expect(store.getActions()).toEqual(expectedActions) + }) + + it('should render message when no libraries uploaded', () => { + (triggeredFunctionsSelector as jest.Mock).mockReturnValueOnce({ + libraries: [], + loading: false + }) + render() + + expect(screen.getByTestId('triggered-functions-welcome')).toBeInTheDocument() + }) + + it('should render libraries list', () => { + (triggeredFunctionsSelector as jest.Mock).mockReturnValueOnce({ + libraries: mockedLibraries, + loading: false + }) + render() + + expect(screen.queryByTestId('triggered-functions-welcome')).not.toBeInTheDocument() + expect(screen.getByTestId('libraries-list-table')).toBeInTheDocument() + expect(screen.queryAllByTestId(/^row-/).length).toEqual(3) + }) + + it('should filter libraries list', () => { + (triggeredFunctionsSelector as jest.Mock).mockReturnValueOnce({ + libraries: mockedLibraries, + loading: false + }) + render() + + fireEvent.change( + screen.getByTestId('search-libraries-list'), + { target: { value: 'lib1' } } + ) + expect(screen.queryAllByTestId(/^row-/).length).toEqual(1) + expect(screen.getByTestId('row-lib1')).toBeInTheDocument() + + fireEvent.change( + screen.getByTestId('search-libraries-list'), + { target: { value: 'user1' } } + ) + expect(screen.queryAllByTestId(/^row-/).length).toEqual(2) + expect(screen.getByTestId('row-lib1')).toBeInTheDocument() + expect(screen.getByTestId('row-lib2')).toBeInTheDocument() + + fireEvent.change( + screen.getByTestId('search-libraries-list'), + { target: { value: '' } } + ) + expect(screen.queryAllByTestId(/^row-/).length).toEqual(3) + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.spec.tsx new file mode 100644 index 0000000000..3437f2cc12 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.spec.tsx @@ -0,0 +1,84 @@ +import React from 'react' + +import { mock } from 'ts-mockito' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' +import { TriggeredFunctionsLibrary } from 'uiSrc/slices/interfaces/triggeredFunctions' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import LibrariesList, { Props } from './LibrariesList' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +const mockedLibraries: TriggeredFunctionsLibrary[] = [ + { + name: 'lib1', + user: 'user1', + totalFunctions: 2, + pendingJobs: 1 + }, + { + name: 'lib2', + user: 'user1', + totalFunctions: 2, + pendingJobs: 1 + }, + { + name: 'lib3', + user: 'user2', + totalFunctions: 2, + pendingJobs: 1 + } +] + +const mockedProps = mock() + +describe('LibrariesList', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render items properly', () => { + render() + + expect(screen.getByTestId('total-libraries')).toHaveTextContent('Total: 3') + expect(screen.queryAllByTestId(/^row-/).length).toEqual(3) + }) + + it('should call proper telemetry events', () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render() + + fireEvent.click(screen.getByTestId('refresh-libraries-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_REFRESH_CLICKED, + }) + + sendEventTelemetry.mockRestore() + + fireEvent.click(screen.getByTestId('auto-refresh-config-btn')) + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_ENABLED, + eventData: { + refreshRate: '5.0' + } + }) + + sendEventTelemetry.mockRestore() + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED, + eventData: { + refreshRate: '5.0' + } + }) + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx index bd9f6247fd..22ada05e35 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx @@ -127,7 +127,8 @@ const LibrariesList = (props: Props) => { responsive={false} rowProps={(row) => ({ onClick: () => handleSelect(row), - className: row.name === selectedRow ? 'selected' : '' + className: row.name === selectedRow ? 'selected' : '', + 'data-testid': `row-${row.name}`, })} message={NoLibrariesMessage} onTableChange={handleSorting} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx new file mode 100644 index 0000000000..8841337f50 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { render } from 'uiSrc/utils/test-utils' + +import NoLibrariesScreen from './NoLibrariesScreen' + +describe('NoLibrariesScreen', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts index e69de29bb2..9d7cb39190 100644 --- a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts +++ b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts @@ -0,0 +1,159 @@ +import { cloneDeep } from 'lodash' +import { AxiosError } from 'axios' +import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' +import reducer, { + fetchTriggeredFunctionsLibrariesList, + getTriggeredFunctionsFailure, + getTriggeredFunctionsList, + getTriggeredFunctionsListSuccess, + initialState, + triggeredFunctionsSelector +} from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { apiService } from 'uiSrc/services' +import { addErrorNotification } from 'uiSrc/slices/app/notifications' + +let store: typeof mockedStore + +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +const timestamp = 1629128049027 +let dateNow: jest.SpyInstance + +describe('triggeredFunctions slice', () => { + beforeAll(() => { + dateNow = jest.spyOn(Date, 'now').mockImplementation(() => timestamp) + }) + + afterAll(() => { + dateNow.mockRestore() + }) + + describe('reducer, actions and selectors', () => { + it('should return the initial state', () => { + // Arrange + const nextState = initialState + + // Act + const result = reducer(undefined, {}) + + // Assert + expect(result).toEqual(nextState) + }) + }) + + describe('getTriggeredFunctionsList', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + loading: true + } + + // Act + const nextState = reducer(initialState, getTriggeredFunctionsList()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('getTriggeredFunctionsListSuccess', () => { + it('should properly set state', () => { + // Arrange + const libraries = [{ name: 'lib1', user: 'user1' }] + const state = { + ...initialState, + lastRefresh: Date.now(), + libraries + } + + // Act + const nextState = reducer(initialState, getTriggeredFunctionsListSuccess(libraries)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('getTriggeredFunctionsFailure', () => { + it('should properly set state', () => { + // Arrange + const error = 'error' + const state = { + ...initialState, + error, + } + + // Act + const nextState = reducer(initialState, getTriggeredFunctionsFailure(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + // thunks + + describe('thunks', () => { + describe('fetchTriggeredFunctionsLibrariesList', () => { + it('succeed to fetch data', async () => { + const data = [{ name: 'lib1', user: 'default' }] + const responsePayload = { data, status: 200 } + + apiService.get = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch( + fetchTriggeredFunctionsLibrariesList('123') + ) + + // Assert + const expectedActions = [ + getTriggeredFunctionsList(), + getTriggeredFunctionsListSuccess(data), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch data', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.get = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch( + fetchTriggeredFunctionsLibrariesList('123') + ) + + // Assert + const expectedActions = [ + getTriggeredFunctionsList(), + addErrorNotification(responsePayload as AxiosError), + getTriggeredFunctionsFailure(errorMessage) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) + }) +}) From 21b70537a0b7af9d098f76d91e3d0462c4870b7a Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 12 Jun 2023 18:42:29 +0200 Subject: [PATCH 011/106] add rte redisgears 2.0 --- tests/e2e/.desktop.env | 9 +++++++ tests/e2e/helpers/conf.ts | 44 +++++++++++++++++++------------- tests/e2e/rte.docker-compose.yml | 7 ++++- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/tests/e2e/.desktop.env b/tests/e2e/.desktop.env index c5d5989d07..552b5e4b3f 100644 --- a/tests/e2e/.desktop.env +++ b/tests/e2e/.desktop.env @@ -15,6 +15,15 @@ OSS_STANDALONE_REDISEARCH_PORT=8102 OSS_STANDALONE_BIG_HOST=localhost OSS_STANDALONE_BIG_PORT=8103 +OSS_STANDALONE_TLS_HOST=localhost +OSS_STANDALONE_TLS_PORT=8104 + +OSS_STANDALONE_EMPTY_HOST=localhost +OSS_STANDALONE_EMPTY_PORT=8105 + +OSS_STANDALONE_REDISGEARS_HOST=localhost +OSS_STANDALONE_REDISGEARS_PORT=8106 + OSS_CLUSTER_HOST=localhost OSS_CLUSTER_PORT=8200 diff --git a/tests/e2e/helpers/conf.ts b/tests/e2e/helpers/conf.ts index 1b263f8d8e..9892381c8d 100644 --- a/tests/e2e/helpers/conf.ts +++ b/tests/e2e/helpers/conf.ts @@ -22,11 +22,11 @@ export const ossStandaloneConfig = { }; export const ossStandaloneConfigEmpty = { - host: process.env.OSS_STANDALONE_HOST || 'oss-standalone-empty', - port: process.env.OSS_STANDALONE_PORT || '6379', - databaseName: `${process.env.OSS_STANDALONE_DATABASE_NAME || 'test_standalone_empty'}-${uniqueId}`, - databaseUsername: process.env.OSS_STANDALONE_USERNAME, - databasePassword: process.env.OSS_STANDALONE_PASSWORD + host: process.env.OSS_STANDALONE_EMPTY_HOST || 'oss-standalone-empty', + port: process.env.OSS_STANDALONE_EMPTY_PORT || '6379', + databaseName: `${process.env.OSS_STANDALONE_EMPTY_DATABASE_NAME || 'test_standalone_empty'}-${uniqueId}`, + databaseUsername: process.env.OSS_STANDALONE_EMPTY_USERNAME, + databasePassword: process.env.OSS_STANDALONE_EMPTY_PASSWORD }; export const ossStandaloneV5Config = { @@ -81,9 +81,9 @@ export const redisEnterpriseClusterConfig = { export const invalidOssStandaloneConfig = { host: 'oss-standalone-invalid', port: '1010', - databaseName: `${process.env.OSS_STANDALONE_DATABASE_NAME || 'test_standalone-invalid'}-${uniqueId}`, - databaseUsername: process.env.OSS_STANDALONE_USERNAME, - databasePassword: process.env.OSS_STANDALONE_PASSWORD + databaseName: `${process.env.OSS_STANDALONE_INVALID_DATABASE_NAME || 'test_standalone-invalid'}-${uniqueId}`, + databaseUsername: process.env.OSS_STANDALONE_INVALID_USERNAME, + databasePassword: process.env.OSS_STANDALONE_INVALID_PASSWORD }; export const ossStandaloneBigConfig = { @@ -103,19 +103,19 @@ export const cloudDatabaseConfig = { }; export const ossStandaloneNoPermissionsConfig = { - host: process.env.OSS_STANDALONE_HOST || 'oss-standalone', - port: process.env.OSS_STANDALONE_PORT || '6379', - databaseName: `${process.env.OSS_STANDALONE_DATABASE_NAME || 'oss-standalone-no-permissions'}-${uniqueId}`, - databaseUsername: process.env.OSS_STANDALONE_USERNAME || 'noperm', - databasePassword: process.env.OSS_STANDALONE_PASSWORD + host: process.env.OSS_STANDALONE_NOPERM_HOST || 'oss-standalone', + port: process.env.OSS_STANDALONE_NOPERM_PORT || '6379', + databaseName: `${process.env.OSS_STANDALONE_NOPERM_DATABASE_NAME || 'oss-standalone-no-permissions'}-${uniqueId}`, + databaseUsername: process.env.OSS_STANDALONE_NOPERM_USERNAME || 'noperm', + databasePassword: process.env.OSS_STANDALONE_NOPERM_PASSWORD }; export const ossStandaloneForSSHConfig = { - host: process.env.OSS_STANDALONE_HOST || '172.33.100.111', - port: process.env.OSS_STANDALONE_PORT || '6379', - databaseName: `${process.env.OSS_STANDALONE_DATABASE_NAME || 'oss-standalone-for-ssh'}-${uniqueId}`, - databaseUsername: process.env.OSS_STANDALONE_USERNAME, - databasePassword: process.env.OSS_STANDALONE_PASSWORD + host: process.env.OSS_STANDALONE_SSH_HOST || '172.33.100.111', + port: process.env.OSS_STANDALONE_SSH_PORT || '6379', + databaseName: `${process.env.OSS_STANDALONE_SSH_DATABASE_NAME || 'oss-standalone-for-ssh'}-${uniqueId}`, + databaseUsername: process.env.OSS_STANDALONE_SSH_USERNAME, + databasePassword: process.env.OSS_STANDALONE_SSH_PASSWORD }; export const ossStandaloneTlsConfig = { @@ -134,3 +134,11 @@ export const ossStandaloneTlsConfig = { key: process.env.E2E_CLIENT_KEY || fs.readFileSync('./rte/oss-standalone-tls/certs/redis.key', 'utf-8') } }; + +export const ossStandaloneRedisGears = { + host: process.env.OSS_STANDALONE_REDISGEARS_HOST || 'oss-standalone-redisgears-2-0', + port: process.env.OSS_STANDALONE_REDISGEARS_PORT || '6379', + databaseName: `${process.env.OSS_STANDALONE_REDISGEARS_DATABASE_NAME || 'test_standalone_redisgears'}-${uniqueId}`, + databaseUsername: process.env.OSS_STANDALONE_REDISGEARS_USERNAME, + databasePassword: process.env.OSS_STANDALONE_REDISGEARS_PASSWORD +}; diff --git a/tests/e2e/rte.docker-compose.yml b/tests/e2e/rte.docker-compose.yml index b27d84da05..108b7e3880 100644 --- a/tests/e2e/rte.docker-compose.yml +++ b/tests/e2e/rte.docker-compose.yml @@ -60,6 +60,11 @@ services: image: redislabs/redismod ports: - 8102:6379 + + oss-standalone-redisgears-2-0: + image: redislabs/redisgears:edge + ports: + - 8106:6379 oss-standalone-big: build: @@ -70,7 +75,7 @@ services: ports: - 8103:6379 - # oss standalone еды + # oss standalone tls oss-standalone-tls: build: context: ./rte/oss-standalone-tls From 6a52cab5bfce629e2d520036255ff0dd8fa8ad28 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Tue, 13 Jun 2023 14:08:52 +0200 Subject: [PATCH 012/106] test#1 for triggered functions --- .../e2e/interfaces/triggers-and-functions.ts | 6 +++ .../components/navigation-panel.ts | 1 + tests/e2e/pageObjects/index.ts | 2 + .../triggers-and-functions-page.ts | 31 +++++++++++++ .../critical-path/tree-view/delimiter.e2e.ts | 2 +- .../triggers-and-functions/libraries.e2e.ts | 43 +++++++++++++++++++ .../insights/live-recommendations.e2e.ts | 2 +- 7 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/interfaces/triggers-and-functions.ts create mode 100644 tests/e2e/pageObjects/triggers-and-functions-page.ts create mode 100644 tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts diff --git a/tests/e2e/interfaces/triggers-and-functions.ts b/tests/e2e/interfaces/triggers-and-functions.ts new file mode 100644 index 0000000000..d590a995f6 --- /dev/null +++ b/tests/e2e/interfaces/triggers-and-functions.ts @@ -0,0 +1,6 @@ +export interface TriggersAndFunctionLibrary { + name: string, + user: string, + pending: number, + totalFunctions: number +} diff --git a/tests/e2e/pageObjects/components/navigation-panel.ts b/tests/e2e/pageObjects/components/navigation-panel.ts index 522031e9bc..f44ca9f5e9 100644 --- a/tests/e2e/pageObjects/components/navigation-panel.ts +++ b/tests/e2e/pageObjects/components/navigation-panel.ts @@ -12,6 +12,7 @@ export class NavigationPanel { browserButton = Selector('[data-testid=browser-page-btn]'); pubSubButton = Selector('[data-testid=pub-sub-page-btn]'); myRedisDBButton = Selector('[data-test-subj=home-page-btn]', { timeout: 1000 }); + triggeredFunctionsButton = Selector('[data-testid=triggered-functions-page-btn]'); notificationCenterButton = Selector('[data-testid=notification-menu-button]'); settingsButton = Selector('[data-testid=settings-page-btn]'); diff --git a/tests/e2e/pageObjects/index.ts b/tests/e2e/pageObjects/index.ts index dba65891de..6d800118c0 100644 --- a/tests/e2e/pageObjects/index.ts +++ b/tests/e2e/pageObjects/index.ts @@ -9,6 +9,7 @@ import { PubSubPage } from './pub-sub-page'; import { SlowLogPage } from './slow-log-page'; import { BasePage } from './base-page'; import { InstancePage } from './instance-page'; +import { TriggersAndFunctionsPage } from './triggers-and-functions-page'; export { AutoDiscoverREDatabases, @@ -22,4 +23,5 @@ export { SlowLogPage, BasePage, InstancePage, + TriggersAndFunctionsPage }; diff --git a/tests/e2e/pageObjects/triggers-and-functions-page.ts b/tests/e2e/pageObjects/triggers-and-functions-page.ts new file mode 100644 index 0000000000..258ff05bca --- /dev/null +++ b/tests/e2e/pageObjects/triggers-and-functions-page.ts @@ -0,0 +1,31 @@ +import { Selector } from 'testcafe'; +import { TriggersAndFunctionLibrary } from '../interfaces/triggers-and-functions'; +import { InstancePage } from './instance-page'; + +export class TriggersAndFunctionsPage extends InstancePage { + //Containers + libraryRow = Selector('[data-testid=row-]'); + /** + * Is library displayed in the table + * @param libraryName The Library Name + */ + getLibraryNameSelector(libraryName: string): Selector { + return Selector(`[data-testid=row-${libraryName}]`); + } + + /** + * Get library item by name + * @param libraryName The Library Name + */ + async getLibraryItem(libraryName: string): Promise { + const item = {} as TriggersAndFunctionLibrary; + const row = this.getLibraryNameSelector(libraryName); + item.name = await row.find('span').nth(0).textContent; + item.user = await row.find('span').nth(1).textContent; + item.pending = parseInt(await row.find('span').nth(2).textContent); + item.totalFunctions = parseInt(await row.find('span').nth(3).textContent); + + return item; + } +} + diff --git a/tests/e2e/tests/critical-path/tree-view/delimiter.e2e.ts b/tests/e2e/tests/critical-path/tree-view/delimiter.e2e.ts index 3eef290ae9..21e6fa5b63 100644 --- a/tests/e2e/tests/critical-path/tree-view/delimiter.e2e.ts +++ b/tests/e2e/tests/critical-path/tree-view/delimiter.e2e.ts @@ -6,7 +6,7 @@ import { commonUrl, ossStandaloneBigConfig } from '../../../helpers/conf'; -import {rte} from '../../../helpers/constants'; +import { rte } from '../../../helpers/constants'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts new file mode 100644 index 0000000000..98a90f2f48 --- /dev/null +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -0,0 +1,43 @@ +import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { BrowserPage, TriggersAndFunctionsPage } from '../../../pageObjects'; +import { + commonUrl, + ossStandaloneBigConfig, ossStandaloneRedisGears +} from '../../../helpers/conf'; +import { rte } from '../../../helpers/constants'; +import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { TriggersAndFunctionLibrary } from '../../../interfaces/triggers-and-functions'; + +const browserPage = new BrowserPage(); +const triggersAndFunctionsPage = new TriggersAndFunctionsPage(); + +const libraryName = 'lib'; + +fixture `Triggers and Functions` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl) + .beforeEach(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisGears, ossStandaloneBigConfig.databaseName); + }) + .afterEach(async() => { + // Delete database + await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); + }); + +test + .after(async() => { + await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); + await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); + })('Verify that when user can see added library', async t => { + + const item = { name: libraryName, user: 'default', pending: 0, totalFunctions: 1 } as TriggersAndFunctionLibrary; + const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n redis.registerFunction(\'foo\', ()=>{return \'bar\'})"`; + await browserPage.Cli.sendCommandInCli(command); + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + const row = await triggersAndFunctionsPage.getLibraryItem(libraryName); + await t.expect(row.name).eql(item.name, 'library name is unexpected'); + await t.expect(row.user).eql(item.user, 'user name is unexpected'); + await t.expect(row.pending).eql(item.pending, 'user name is unexpected'); + await t.expect(row.totalFunctions).eql(item.totalFunctions, 'user name is unexpected'); + }); + diff --git a/tests/e2e/tests/regression/insights/live-recommendations.e2e.ts b/tests/e2e/tests/regression/insights/live-recommendations.e2e.ts index bf43433c69..575168c431 100644 --- a/tests/e2e/tests/regression/insights/live-recommendations.e2e.ts +++ b/tests/e2e/tests/regression/insights/live-recommendations.e2e.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { BrowserPage, MemoryEfficiencyPage, MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { RecommendationIds, rte } from '../../../helpers/constants'; -import { acceptLicenseTerms, acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { acceptLicenseTerms } from '../../../helpers/database'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../helpers/conf'; import { addNewStandaloneDatabaseApi, From b78320db926cea8fe7d68561bec9471e40eda333 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 14 Jun 2023 12:22:45 +0200 Subject: [PATCH 013/106] #RI-4635 - add spinner #RI-4631 - fix telemetry event data --- .../pages/Libraries/LibrariesPage.tsx | 17 ++++++++++++----- .../LibrariesList/LibrariesList.spec.tsx | 9 +++++++-- .../components/LibrariesList/LibrariesList.tsx | 14 ++++++++++++-- .../pages/Libraries/styles.module.scss | 18 ++++++++++++++++++ 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx index 37774c772b..01b4ac6806 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx @@ -1,5 +1,11 @@ import React, { useEffect, useState } from 'react' -import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui' +import { + EuiButton, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, +} from '@elastic/eui' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' @@ -51,10 +57,6 @@ const LibrariesPage = () => { setItems(itemsTemp || []) } - if (!libraries) { - return <> - } - return ( @@ -92,6 +94,11 @@ const LibrariesPage = () => { + {!libraries && loading && ( +
+ +
+ )} {(libraries?.length > 0) && ( { expect(sendEventTelemetry).toBeCalledWith({ event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_REFRESH_CLICKED, + eventData: { + databaseId: 'instanceId' + } }) sendEventTelemetry.mockRestore() @@ -67,7 +70,8 @@ describe('LibrariesList', () => { expect(sendEventTelemetry).toBeCalledWith({ event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_ENABLED, eventData: { - refreshRate: '5.0' + refreshRate: '5.0', + databaseId: 'instanceId' } }) @@ -77,7 +81,8 @@ describe('LibrariesList', () => { expect(sendEventTelemetry).toBeCalledWith({ event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED, eventData: { - refreshRate: '5.0' + refreshRate: '5.0', + databaseId: 'instanceId' } }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx index 22ada05e35..37d538c3a7 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react' import { EuiBasicTableColumn, EuiInMemoryTable, EuiText, EuiToolTip, PropertySort } from '@elastic/eui' import cx from 'classnames' +import { useParams } from 'react-router-dom' import { Maybe, Nullable } from 'uiSrc/utils' import AutoRefresh from 'uiSrc/pages/browser/components/auto-refresh' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' @@ -22,6 +23,8 @@ const LibrariesList = (props: Props) => { const [sort, setSort] = useState>(undefined) const [selectedRow, setSelectedRow] = useState>(null) + const { instanceId } = useParams<{ instanceId: string }>() + const columns: EuiBasicTableColumn[] = [ { name: 'Library Name', @@ -81,6 +84,9 @@ const LibrariesList = (props: Props) => { const handleRefreshClicked = () => { sendEventTelemetry({ event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_REFRESH_CLICKED, + eventData: { + databaseId: instanceId + } }) } @@ -88,7 +94,10 @@ const LibrariesList = (props: Props) => { setSort(sort) sendEventTelemetry({ event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARIES_SORTED, - eventData: sort + eventData: { + ...sort, + databaseId: instanceId + } }) } @@ -98,7 +107,8 @@ const LibrariesList = (props: Props) => { ? TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_ENABLED : TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED, eventData: { - refreshRate + refreshRate, + databaseId: instanceId } }) } diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss index cb80391a5c..99e201954b 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss @@ -21,4 +21,22 @@ .main { background-color: var(--euiColorEmptyShade); max-height: calc(100% - 72px); + + .loading { + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; + + flex-direction: column; + + :global { + .euiLoadingSpinner { + width: 40px; + height: 40px; + } + } + } } From 68d7fb93d6a1d66d31287809ca93a2f5b56d5c50 Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Mon, 19 Jun 2023 14:11:04 +0300 Subject: [PATCH 014/106] #RI-4646 - add upload and replace library endpoints (#2206) --- .../modules/triggered-functions/dto/index.ts | 1 + .../dto/upload-library.dto.ts | 26 ++++++ .../triggered-functions.controller.ts | 26 +++++- .../triggered-functions.service.spec.ts | 80 +++++++++++++++++++ .../triggered-functions.service.ts | 45 +++++++++++ 5 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 redisinsight/api/src/modules/triggered-functions/dto/upload-library.dto.ts diff --git a/redisinsight/api/src/modules/triggered-functions/dto/index.ts b/redisinsight/api/src/modules/triggered-functions/dto/index.ts index 1f35351615..1152e6c8d2 100644 --- a/redisinsight/api/src/modules/triggered-functions/dto/index.ts +++ b/redisinsight/api/src/modules/triggered-functions/dto/index.ts @@ -1 +1,2 @@ export * from './library.dto'; +export * from './upload-library.dto'; diff --git a/redisinsight/api/src/modules/triggered-functions/dto/upload-library.dto.ts b/redisinsight/api/src/modules/triggered-functions/dto/upload-library.dto.ts new file mode 100644 index 0000000000..8f4c336ae5 --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/dto/upload-library.dto.ts @@ -0,0 +1,26 @@ +import { + IsDefined, + IsOptional, +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsRedisString, RedisStringType } from 'src/common/decorators'; + +export class UploadLibraryDto { + @ApiProperty({ + description: 'Library code', + type: String, + }) + @IsRedisString() + @RedisStringType() + @IsDefined() + code: string; + + @ApiPropertyOptional({ + description: 'Library config', + type: String, + }) + @IsOptional() + @IsRedisString() + @RedisStringType() + config?: string; +} diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts index 635898f7d5..68240c91c6 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts @@ -7,7 +7,7 @@ import { ApiTags } from '@nestjs/swagger'; import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; import { TriggeredFunctionsService } from 'src/modules/triggered-functions/triggered-functions.service'; import { ShortLibrary, Library, Function } from 'src/modules/triggered-functions/models'; -import { LibraryDto } from 'src/modules/triggered-functions/dto'; +import { LibraryDto, UploadLibraryDto } from 'src/modules/triggered-functions/dto'; import { ClientMetadata } from 'src/common/models'; import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; @@ -71,4 +71,28 @@ export class TriggeredFunctionsController { ): Promise { return this.service.functionsList(clientMetadata); } + + @Post('library') + @ApiRedisInstanceOperation({ + description: 'Upload new library', + statusCode: 201, + }) + async upload( + @BrowserClientMetadata() clientMetadata: ClientMetadata, + @Body() dto: UploadLibraryDto, + ): Promise { + return this.service.upload(clientMetadata, dto); + } + + @Post('library/replace') + @ApiRedisInstanceOperation({ + description: 'Upgrade existing library', + statusCode: 201, + }) + async upgrade( + @BrowserClientMetadata() clientMetadata: ClientMetadata, + @Body() dto: UploadLibraryDto, + ): Promise { + return this.service.upload(clientMetadata, dto, true); + } } diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts index 7103a4ddd9..313c7e1bbf 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts @@ -42,6 +42,10 @@ const mockTFunctionsVerboseReply = [[ const mockLibraryName = 'name'; +const mockCode = '#!js api_version=1.0 name=lib'; + +const mockConfig = '{}'; + describe('TriggeredFunctionsService', () => { let service: TriggeredFunctionsService; let databaseConnectionService: MockType; @@ -219,4 +223,80 @@ describe('TriggeredFunctionsService', () => { } }); }); + + describe('upload', () => { + it('should upload library', async () => { + mockIORedisClient.sendCommand.mockResolvedValueOnce(mockLibrariesReply); + await service.upload(mockClientMetadata, { code: mockCode }); + + expect(mockIORedisClient.sendCommand).toHaveBeenCalledTimes(1); + expect(mockIORedisClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining({ + name: 'TFUNCTION', + args: ['LOAD', mockCode], + })); + }); + + it('should upload library with config', async () => { + mockIORedisClient.sendCommand.mockResolvedValueOnce(mockLibrariesReply); + await service.upload(mockClientMetadata, { code: mockCode, config: mockConfig }); + + expect(mockIORedisClient.sendCommand).toHaveBeenCalledTimes(1); + expect(mockIORedisClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining({ + name: 'TFUNCTION', + args: ['LOAD', 'CONFIG', mockConfig, mockCode], + })); + }); + + it('should replace library', async () => { + mockIORedisClient.sendCommand.mockResolvedValueOnce(mockLibrariesReply); + await service.upload(mockClientMetadata, { code: mockCode }, true); + + expect(mockIORedisClient.sendCommand).toHaveBeenCalledTimes(1); + expect(mockIORedisClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining({ + name: 'TFUNCTION', + args: ['LOAD', 'REPLACE', mockCode], + })); + }); + + it('should replace library with config', async () => { + mockIORedisClient.sendCommand.mockResolvedValueOnce(mockLibrariesReply); + await service.upload(mockClientMetadata, { code: mockCode, config: mockConfig }, true); + + expect(mockIORedisClient.sendCommand).toHaveBeenCalledTimes(1); + expect(mockIORedisClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining({ + name: 'TFUNCTION', + args: ['LOAD', 'REPLACE', 'CONFIG', mockConfig, mockCode], + })); + }); + + it('Should throw Error when error during creating a client in upload', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new Error()); + await service.upload(mockClientMetadata, { code: mockCode }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } + }); + + it('should handle acl error', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new Error('NOPERM')); + await service.upload(mockClientMetadata, { code: mockCode }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(ForbiddenException); + } + }); + + it('should handle HTTP error', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new NotFoundException('Not Found')); + await service.upload(mockClientMetadata, { code: mockCode }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); + }); }); diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts index 4caf5adb79..9d210c9a9a 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts @@ -8,6 +8,7 @@ import { ShortLibrary, Library, Function } from 'src/modules/triggered-functions import { getLibraryInformation, getShortLibraryInformation, getLibraryFunctions, } from 'src/modules/triggered-functions/utils'; +import { UploadLibraryDto } from 'src/modules/triggered-functions/dto'; import { ClientMetadata } from 'src/common/models'; @Injectable() @@ -106,4 +107,48 @@ export class TriggeredFunctionsService { throw catchAclError(e); } } + + /** + * Upload triggered functions library + * @param clientMetadata + * @param dto + * @param isExist + */ + async upload( + clientMetadata: ClientMetadata, + dto: UploadLibraryDto, + isExist = false, + ): Promise { + let client; + try { + const { + code, config, + } = dto; + + const commandArgs: any[] = isExist ? ['LOAD', 'REPLACE'] : ['LOAD']; + + if (config) { + commandArgs.push('CONFIG', config); + } + + commandArgs.push(code); + + client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); + await client.sendCommand( + new Command('TFUNCTION', [...commandArgs], { replyEncoding: 'utf8' }), + ); + + this.logger.log('Succeed to upload library.'); + + return undefined; + } catch (e) { + this.logger.error('Unable to upload library', e); + + if (e instanceof HttpException) { + throw e; + } + + throw catchAclError(e); + } + } } From 4fe2abe586089378c3419b1946f639454702efa7 Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Mon, 19 Jun 2023 16:20:38 +0300 Subject: [PATCH 015/106] #RI-4646 - replace config with configuration (#2208) --- .../modules/triggered-functions/dto/upload-library.dto.ts | 4 ++-- .../triggered-functions.service.spec.ts | 8 ++++---- .../triggered-functions/triggered-functions.service.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/redisinsight/api/src/modules/triggered-functions/dto/upload-library.dto.ts b/redisinsight/api/src/modules/triggered-functions/dto/upload-library.dto.ts index 8f4c336ae5..a7cddf2726 100644 --- a/redisinsight/api/src/modules/triggered-functions/dto/upload-library.dto.ts +++ b/redisinsight/api/src/modules/triggered-functions/dto/upload-library.dto.ts @@ -16,11 +16,11 @@ export class UploadLibraryDto { code: string; @ApiPropertyOptional({ - description: 'Library config', + description: 'Library configuration', type: String, }) @IsOptional() @IsRedisString() @RedisStringType() - config?: string; + configuration?: string; } diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts index 313c7e1bbf..f82a959889 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts @@ -236,9 +236,9 @@ describe('TriggeredFunctionsService', () => { })); }); - it('should upload library with config', async () => { + it('should upload library with configuration', async () => { mockIORedisClient.sendCommand.mockResolvedValueOnce(mockLibrariesReply); - await service.upload(mockClientMetadata, { code: mockCode, config: mockConfig }); + await service.upload(mockClientMetadata, { code: mockCode, configuration: mockConfig }); expect(mockIORedisClient.sendCommand).toHaveBeenCalledTimes(1); expect(mockIORedisClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining({ @@ -258,9 +258,9 @@ describe('TriggeredFunctionsService', () => { })); }); - it('should replace library with config', async () => { + it('should replace library with configuration', async () => { mockIORedisClient.sendCommand.mockResolvedValueOnce(mockLibrariesReply); - await service.upload(mockClientMetadata, { code: mockCode, config: mockConfig }, true); + await service.upload(mockClientMetadata, { code: mockCode, configuration: mockConfig }, true); expect(mockIORedisClient.sendCommand).toHaveBeenCalledTimes(1); expect(mockIORedisClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining({ diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts index 9d210c9a9a..38a1692f4b 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts @@ -122,13 +122,13 @@ export class TriggeredFunctionsService { let client; try { const { - code, config, + code, configuration, } = dto; const commandArgs: any[] = isExist ? ['LOAD', 'REPLACE'] : ['LOAD']; - if (config) { - commandArgs.push('CONFIG', config); + if (configuration) { + commandArgs.push('CONFIG', configuration); } commandArgs.push(code); From ef10ced35830909de3011336aac5c90b5ad142c3 Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Tue, 20 Jun 2023 14:46:55 +0300 Subject: [PATCH 016/106] #RI-4591 - add NotFoundException error (#2212) --- redisinsight/api/src/constants/error-messages.ts | 1 + .../triggered-functions.service.spec.ts | 9 +++++++++ .../triggered-functions.service.ts | 12 +++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/redisinsight/api/src/constants/error-messages.ts b/redisinsight/api/src/constants/error-messages.ts index dba176f778..6df97b45cb 100644 --- a/redisinsight/api/src/constants/error-messages.ts +++ b/redisinsight/api/src/constants/error-messages.ts @@ -64,4 +64,5 @@ export default { APP_SETTINGS_NOT_FOUND: () => 'Could not find application settings.', SERVER_INFO_NOT_FOUND: () => 'Could not find server info.', INCREASE_MINIMUM_LIMIT: (count: string) => `Set MAXSEARCHRESULTS to at least ${count}.`, + LIBRARY_NOT_EXIST: 'This library does not exist.', }; diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts index f82a959889..6dd77ddd1b 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts @@ -175,6 +175,15 @@ describe('TriggeredFunctionsService', () => { expect(e).toBeInstanceOf(NotFoundException); } }); + + it('should return NotFoundException when library does not exist', async () => { + mockIORedisClient.sendCommand.mockResolvedValueOnce([]); + + await expect( + service.details(mockClientMetadata, mockLibraryName), + ).rejects.toThrow(NotFoundException); + }); + }); describe('libraryList', () => { diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts index 38a1692f4b..116a357a33 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts @@ -1,5 +1,5 @@ import { Command } from 'ioredis'; -import { HttpException, Injectable, Logger } from '@nestjs/common'; +import { HttpException, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { catchAclError } from 'src/utils'; import { concat } from 'lodash'; import { plainToClass } from 'class-transformer'; @@ -10,6 +10,7 @@ import { } from 'src/modules/triggered-functions/utils'; import { UploadLibraryDto } from 'src/modules/triggered-functions/dto'; import { ClientMetadata } from 'src/common/models'; +import ERROR_MESSAGES from 'src/constants/error-messages'; @Injectable() export class TriggeredFunctionsService { @@ -63,6 +64,15 @@ export class TriggeredFunctionsService { const reply = await client.sendCommand( new Command('TFUNCTION', ['LIST', 'WITHCODE', 'LIBRARY', name], { replyEncoding: 'utf8' }), ); + + if (!reply.length) { + this.logger.error( + `Failed to get library details. Not Found library: ${name}.`, + ); + return Promise.reject( + new NotFoundException(ERROR_MESSAGES.LIBRARY_NOT_EXIST), + ); + } const library = getLibraryInformation(reply[0]); return plainToClass( Library, From 609411c144b377d3c0f3960093ef07c666666c06 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 20 Jun 2023 16:46:00 +0200 Subject: [PATCH 017/106] #RI-4591 - add library details for triggered functions --- configs/webpack.config.renderer.dev.babel.js | 2 +- configs/webpack.config.renderer.prod.babel.js | 2 +- configs/webpack.config.web.common.babel.js | 2 +- redisinsight/__mocks__/monacoMock.js | 8 +- .../monaco-editor/MonacoEditor.spec.tsx | 10 + .../components/monaco-editor/MonacoEditor.tsx | 153 ++++++++++ .../components/monaco-js/MonacoJS.spec.tsx | 10 + .../components/monaco-js/MonacoJS.tsx | 25 ++ .../components/monaco-js/index.ts | 3 + .../monaco-json/MonacoJson.spec.tsx | 2 +- .../components/monaco-json/MonacoJson.tsx | 32 ++ .../components}/monaco-json/index.ts | 0 .../ui/src/components/monaco-editor/index.ts | 9 + .../monaco-editor/styles.modules.scss | 65 ++++ .../src/components/monaco-json/MonacoJson.tsx | 91 ------ .../monaco-json/styles.modules.scss | 24 -- redisinsight/ui/src/constants/api.ts | 2 + redisinsight/ui/src/mocks/handlers/index.ts | 2 + .../handlers/triggeredFunctions/index.ts | 6 + .../triggeredFunctionsHandler.ts | 33 ++ .../add-key/AddKeyReJSON/AddKeyReJSON.tsx | 2 +- .../components/auto-refresh/AutoRefresh.tsx | 2 +- .../pages/Libraries/LibrariesPage.spec.tsx | 16 +- .../pages/Libraries/LibrariesPage.tsx | 103 +++++-- .../LibrariesList/LibrariesList.tsx | 7 +- .../LibraryDetails/LibraryDetails.spec.tsx | 142 +++++++++ .../LibraryDetails/LibraryDetails.tsx | 271 ++++++++++++++++ .../components/LibraryDetails/index.ts | 3 + .../LibraryDetails/styles.module.scss | 87 ++++++ .../pages/Libraries/styles.module.scss | 50 ++- .../slices/interfaces/triggeredFunctions.ts | 26 +- .../triggeredFunctions.spec.ts | 289 +++++++++++++++++- .../triggeredFunctions/triggeredFunctions.ts | 126 +++++++- .../ui/src/styles/components/_tabs.scss | 2 +- redisinsight/ui/src/telemetry/events.ts | 8 +- 35 files changed, 1439 insertions(+), 176 deletions(-) create mode 100644 redisinsight/ui/src/components/monaco-editor/MonacoEditor.spec.tsx create mode 100644 redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx create mode 100644 redisinsight/ui/src/components/monaco-editor/components/monaco-js/MonacoJS.spec.tsx create mode 100644 redisinsight/ui/src/components/monaco-editor/components/monaco-js/MonacoJS.tsx create mode 100644 redisinsight/ui/src/components/monaco-editor/components/monaco-js/index.ts rename redisinsight/ui/src/components/{ => monaco-editor/components}/monaco-json/MonacoJson.spec.tsx (88%) create mode 100644 redisinsight/ui/src/components/monaco-editor/components/monaco-json/MonacoJson.tsx rename redisinsight/ui/src/components/{ => monaco-editor/components}/monaco-json/index.ts (100%) create mode 100644 redisinsight/ui/src/components/monaco-editor/index.ts create mode 100644 redisinsight/ui/src/components/monaco-editor/styles.modules.scss delete mode 100644 redisinsight/ui/src/components/monaco-json/MonacoJson.tsx delete mode 100644 redisinsight/ui/src/components/monaco-json/styles.modules.scss create mode 100644 redisinsight/ui/src/mocks/handlers/triggeredFunctions/index.ts create mode 100644 redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/styles.module.scss diff --git a/configs/webpack.config.renderer.dev.babel.js b/configs/webpack.config.renderer.dev.babel.js index 19726262dd..f3fb8a1069 100644 --- a/configs/webpack.config.renderer.dev.babel.js +++ b/configs/webpack.config.renderer.dev.babel.js @@ -227,7 +227,7 @@ export default merge(baseConfig, { new ReactRefreshWebpackPlugin(), - new MonacoWebpackPlugin({ languages: ['json'], features: ['!rename'] }), + new MonacoWebpackPlugin({ languages: ['json', 'javascript', 'typescript'], features: ['!rename'] }), ], node: { diff --git a/configs/webpack.config.renderer.prod.babel.js b/configs/webpack.config.renderer.prod.babel.js index f0c894b9c1..2d7fac5ab0 100644 --- a/configs/webpack.config.renderer.prod.babel.js +++ b/configs/webpack.config.renderer.prod.babel.js @@ -188,7 +188,7 @@ export default merge(baseConfig, { }, plugins: [ - new MonacoWebpackPlugin({ languages: ['json'], features: ['!rename'] }), + new MonacoWebpackPlugin({ languages: ['json', 'javascript', 'typescript'], features: ['!rename'] }), new webpack.EnvironmentPlugin({ NODE_ENV: 'production', diff --git a/configs/webpack.config.web.common.babel.js b/configs/webpack.config.web.common.babel.js index 973b9ea30c..0f587b75b2 100644 --- a/configs/webpack.config.web.common.babel.js +++ b/configs/webpack.config.web.common.babel.js @@ -71,7 +71,7 @@ export default { plugins: [ new HtmlWebpackPlugin({ template: 'index.html.ejs' }), - new MonacoWebpackPlugin({ languages: ['json'], features: ['!rename'] }), + new MonacoWebpackPlugin({ languages: ['json', 'javascript', 'typescript'], features: ['!rename'] }), new webpack.IgnorePlugin({ checkResource(resource) { diff --git a/redisinsight/__mocks__/monacoMock.js b/redisinsight/__mocks__/monacoMock.js index e04f14c09b..23881a7faf 100644 --- a/redisinsight/__mocks__/monacoMock.js +++ b/redisinsight/__mocks__/monacoMock.js @@ -15,8 +15,10 @@ export default function MonacoEditor(props) { createContextKey: jest.fn(), focus: jest.fn(), onDidChangeCursorPosition: jest.fn(), + onDidAttemptReadOnlyEdit: jest.fn(), executeEdits: jest.fn(), - updateOptions: jest.fn() + updateOptions: jest.fn(), + setSelection: jest.fn(), }, // monaco { @@ -57,3 +59,7 @@ export const languages = { InsertAsSnippet: 4 } } + +export const monaco = { + Selection: jest.fn().mockImplementation(() => { return {} }) +} diff --git a/redisinsight/ui/src/components/monaco-editor/MonacoEditor.spec.tsx b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.spec.tsx new file mode 100644 index 0000000000..e03e49da75 --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { render } from 'uiSrc/utils/test-utils' + +import MonacoEditor from './MonacoEditor' + +describe('MonacoEditor', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx new file mode 100644 index 0000000000..7694add1e1 --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx @@ -0,0 +1,153 @@ +import React, { useContext, useEffect, useRef, useState } from 'react' +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api' +import ReactMonacoEditor, { monaco } from 'react-monaco-editor' +import cx from 'classnames' +import { EuiButton, EuiIcon } from '@elastic/eui' +import { darkTheme, lightTheme, MonacoThemes } from 'uiSrc/constants/monaco/cypher' + +import { Nullable } from 'uiSrc/utils' +import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' +import { Theme } from 'uiSrc/constants' +import { ThemeContext } from 'uiSrc/contexts/themeContext' +import InlineItemEditor from 'uiSrc/components/inline-item-editor' +import styles from './styles.modules.scss' + +export interface CommonProps { + value: string + onChange: (value: string) => void + onApply?: ( + event: React.MouseEvent, + closeEditor: () => void + ) => void + onDecline?: (event?: React.MouseEvent) => void + disabled?: boolean + readOnly?: boolean + isEditable?: boolean + wrapperClassName?: string + options?: monacoEditor.editor.IStandaloneEditorConstructionOptions + 'data-testid'?: string +} + +export interface Props extends CommonProps { + onEditorDidMount?: (editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: typeof monacoEditor) => void + className?: string + language: string +} +const MonacoEditor = (props: Props) => { + const { + value, + onChange, + onApply, + onDecline, + onEditorDidMount, + disabled, + readOnly, + isEditable, + language, + wrapperClassName, + className, + options = {}, + 'data-testid': dataTestId + } = props + + const [isEditing, setIsEditing] = useState(!readOnly && !disabled) + const monacoObjects = useRef>(null) + const valueRef = useRef('') + + const { theme } = useContext(ThemeContext) + + useEffect(() => { + monacoObjects.current?.editor.updateOptions({ readOnly: !isEditing && (disabled || readOnly) }) + }, [disabled, readOnly, isEditing]) + + useEffect(() => { + // clear selection after update empty value + // https://github.com/react-monaco-editor/react-monaco-editor/issues/539 + if (value && !valueRef.current) { + monacoObjects.current?.editor.setSelection(new monaco.Selection(0, 0, 0, 0)) + } + + valueRef.current = value + }, [value]) + + const editorDidMount = ( + editor: monacoEditor.editor.IStandaloneCodeEditor, + monaco: typeof monacoEditor, + ) => { + monacoObjects.current = { editor, monaco } + onEditorDidMount?.(editor, monaco) + } + + if (monaco?.editor) { + monaco.editor.defineTheme(MonacoThemes.Dark, darkTheme) + monaco.editor.defineTheme(MonacoThemes.Light, lightTheme) + } + + const monacoOptions: monacoEditor.editor.IStandaloneEditorConstructionOptions = { + wordWrap: 'on', + automaticLayout: true, + formatOnPaste: false, + padding: { top: 10 }, + suggest: { + preview: false, + showStatusBar: false, + showIcons: false, + showProperties: false, + }, + quickSuggestions: false, + minimap: { + enabled: false, + }, + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + overviewRulerBorder: false, + lineNumbersMinChars: 4, + ...options + } + + const handleApply = (_value: string, event: React.MouseEvent) => { + onApply?.(event, () => setIsEditing(false)) + } + + const handleDecline = (event?: React.MouseEvent) => { + setIsEditing(false) + onDecline?.(event) + } + + return ( +
+ +
+ +
+
+ {isEditable && readOnly && !isEditing && ( + setIsEditing(true)} + className={styles.editBtn} + > + + + )} +
+ ) +} + +export default MonacoEditor diff --git a/redisinsight/ui/src/components/monaco-editor/components/monaco-js/MonacoJS.spec.tsx b/redisinsight/ui/src/components/monaco-editor/components/monaco-js/MonacoJS.spec.tsx new file mode 100644 index 0000000000..c289bd3482 --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/components/monaco-js/MonacoJS.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { render } from 'uiSrc/utils/test-utils' + +import MonacoJS from './MonacoJS' + +describe('MonacoJS', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/components/monaco-editor/components/monaco-js/MonacoJS.tsx b/redisinsight/ui/src/components/monaco-editor/components/monaco-js/MonacoJS.tsx new file mode 100644 index 0000000000..39e90cd1af --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/components/monaco-js/MonacoJS.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api' + +import { MonacoEditor } from 'uiSrc/components/monaco-editor' +import { CommonProps } from 'uiSrc/components/monaco-editor/MonacoEditor' + +const MonacoJS = (props: CommonProps) => { + const editorDidMount = ( + editor: monacoEditor.editor.IStandaloneCodeEditor + ) => { + const messageContribution = editor.getContribution('editor.contrib.messageController') + editor.onDidAttemptReadOnlyEdit(() => messageContribution.dispose()) + } + + return ( + + ) +} + +export default MonacoJS diff --git a/redisinsight/ui/src/components/monaco-editor/components/monaco-js/index.ts b/redisinsight/ui/src/components/monaco-editor/components/monaco-js/index.ts new file mode 100644 index 0000000000..f9f33b1669 --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/components/monaco-js/index.ts @@ -0,0 +1,3 @@ +import MonacoJS from './MonacoJS' + +export default MonacoJS diff --git a/redisinsight/ui/src/components/monaco-json/MonacoJson.spec.tsx b/redisinsight/ui/src/components/monaco-editor/components/monaco-json/MonacoJson.spec.tsx similarity index 88% rename from redisinsight/ui/src/components/monaco-json/MonacoJson.spec.tsx rename to redisinsight/ui/src/components/monaco-editor/components/monaco-json/MonacoJson.spec.tsx index 74f4ce161c..a73089ef8b 100644 --- a/redisinsight/ui/src/components/monaco-json/MonacoJson.spec.tsx +++ b/redisinsight/ui/src/components/monaco-editor/components/monaco-json/MonacoJson.spec.tsx @@ -3,7 +3,7 @@ import { render } from 'uiSrc/utils/test-utils' import MonacoJson from './MonacoJson' -describe('', () => { +describe('MonacoJson', () => { it('should render', () => { expect(render()).toBeTruthy() }) diff --git a/redisinsight/ui/src/components/monaco-editor/components/monaco-json/MonacoJson.tsx b/redisinsight/ui/src/components/monaco-editor/components/monaco-json/MonacoJson.tsx new file mode 100644 index 0000000000..4a796d1341 --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/components/monaco-json/MonacoJson.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api' + +import { MonacoEditor } from 'uiSrc/components/monaco-editor' +import { CommonProps } from 'uiSrc/components/monaco-editor/MonacoEditor' + +const MonacoJson = (props: CommonProps) => { + const editorDidMount = ( + editor: monacoEditor.editor.IStandaloneCodeEditor, + monaco: typeof monacoEditor, + ) => { + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + schemaValidation: 'error', + schemaRequest: 'error', + trailingCommas: 'error' + }) + const messageContribution = editor.getContribution('editor.contrib.messageController') + editor.onDidAttemptReadOnlyEdit(() => messageContribution.dispose()) + } + + return ( + + ) +} + +export default MonacoJson diff --git a/redisinsight/ui/src/components/monaco-json/index.ts b/redisinsight/ui/src/components/monaco-editor/components/monaco-json/index.ts similarity index 100% rename from redisinsight/ui/src/components/monaco-json/index.ts rename to redisinsight/ui/src/components/monaco-editor/components/monaco-json/index.ts diff --git a/redisinsight/ui/src/components/monaco-editor/index.ts b/redisinsight/ui/src/components/monaco-editor/index.ts new file mode 100644 index 0000000000..cbd6f985f9 --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/index.ts @@ -0,0 +1,9 @@ +import MonacoEditor from './MonacoEditor' +import MonacoJS from './components/monaco-js' +import MonacoJson from './components/monaco-json' + +export { + MonacoEditor, + MonacoJS, + MonacoJson, +} diff --git a/redisinsight/ui/src/components/monaco-editor/styles.modules.scss b/redisinsight/ui/src/components/monaco-editor/styles.modules.scss new file mode 100644 index 0000000000..23b67dba8f --- /dev/null +++ b/redisinsight/ui/src/components/monaco-editor/styles.modules.scss @@ -0,0 +1,65 @@ +.wrapper { + position: relative; + height: 200px; + max-width: 100% !important; + border: 1px solid var(--controlsBorderColor) !important; + + :global(.inlineMonacoEditor) { + height: 192px; + width: 100%; + font-size: 14px; + line-height: 24px; + letter-spacing: -0.14px; + margin-bottom: 2px; + } + + &:global(.disabled) { + pointer-events: none; + opacity: 0.5; + } + + :global { + .monaco-editor, .monaco-editor .margin, .monaco-editor .minimap-decorations-layer, .monaco-editor-background { + background-color: var(--euiColorEmptyShade) !important; + } + + .monaco-editor .hover-row.status-bar { + display: none; + } + } + + .isEditing { + padding-bottom: 40px; + } + + .editBtn { + display: flex; + align-items: center; + justify-content: center; + + position: absolute; + bottom: 16px; + right: 16px; + + width: 30px !important; + min-width: 0 !important; + height: 30px !important; + border-radius: 100%; + + opacity: 0.7; + + &:hover { + opacity: 1; + } + + :global { + .euiButton__content { + padding: 0 !important; + } + + .euiButton__text { + line-height: 12px !important; + } + } + } +} diff --git a/redisinsight/ui/src/components/monaco-json/MonacoJson.tsx b/redisinsight/ui/src/components/monaco-json/MonacoJson.tsx deleted file mode 100644 index 3dabe57c7d..0000000000 --- a/redisinsight/ui/src/components/monaco-json/MonacoJson.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useContext, useEffect, useRef } from 'react' -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api' -import MonacoEditor, { monaco } from 'react-monaco-editor' -import cx from 'classnames' -import { darkTheme, lightTheme, MonacoThemes } from 'uiSrc/constants/monaco/cypher' - -import { Nullable } from 'uiSrc/utils' -import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' -import { Theme } from 'uiSrc/constants' -import { ThemeContext } from 'uiSrc/contexts/themeContext' -import styles from './styles.modules.scss' - -export interface Props { - value: string - onChange: (value: string) => void - disabled?: boolean - wrapperClassName?: string - 'data-testid'?: string -} -const MonacoJson = (props: Props) => { - const { - value, - onChange, - disabled, - wrapperClassName, - 'data-testid': dataTestId - } = props - const monacoObjects = useRef>(null) - - const { theme } = useContext(ThemeContext) - - useEffect(() => { - monacoObjects.current?.editor.updateOptions({ readOnly: disabled }) - }, [disabled]) - - const editorDidMount = ( - editor: monacoEditor.editor.IStandaloneCodeEditor, - monaco: typeof monacoEditor, - ) => { - monacoObjects.current = { editor, monaco } - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - validate: true, - schemaValidation: 'error', - schemaRequest: 'error', - trailingCommas: 'error' - }) - } - - if (monaco?.editor) { - monaco.editor.defineTheme(MonacoThemes.Dark, darkTheme) - monaco.editor.defineTheme(MonacoThemes.Light, lightTheme) - } - - const options: monacoEditor.editor.IStandaloneEditorConstructionOptions = { - wordWrap: 'on', - automaticLayout: true, - formatOnPaste: false, - padding: { top: 10 }, - suggest: { - preview: false, - showStatusBar: false, - showIcons: false, - showProperties: false, - }, - quickSuggestions: false, - minimap: { - enabled: false, - }, - overviewRulerLanes: 0, - hideCursorInOverviewRuler: true, - overviewRulerBorder: false, - lineNumbersMinChars: 4, - } - - return ( -
- -
- ) -} - -export default MonacoJson diff --git a/redisinsight/ui/src/components/monaco-json/styles.modules.scss b/redisinsight/ui/src/components/monaco-json/styles.modules.scss deleted file mode 100644 index 9547189334..0000000000 --- a/redisinsight/ui/src/components/monaco-json/styles.modules.scss +++ /dev/null @@ -1,24 +0,0 @@ -.wrapper { - height: 200px; - max-width: 100% !important; - border: 1px solid var(--controlsBorderColor) !important; - box-shadow: none !important; - font-size: 14px; - line-height: 24px; - letter-spacing: -0.14px; - - &:global(.disabled) { - pointer-events: none; - opacity: 0.5; - } - - :global { - .monaco-editor, .monaco-editor .margin, .monaco-editor .minimap-decorations-layer, .monaco-editor-background { - background-color: var(--euiColorEmptyShade) !important; - } - - .monaco-editor .hover-row.status-bar { - display: none; - } - } -} diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index 0c4d53ea66..a3b249a35c 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -105,6 +105,8 @@ enum ApiEndpoints { RECOMMENDATIONS_READ = 'recommendations/read', TRIGGERED_FUNCTIONS_LIBRARIES = 'triggered-functions/libraries', + TRIGGERED_FUNCTIONS_GET_LIBRARY = 'triggered-functions/get-library', + TRIGGERED_FUNCTIONS_REPLACE_LIBRARY = 'triggered-functions/library/replace', NOTIFICATIONS = 'notifications', NOTIFICATIONS_READ = 'notifications/read', diff --git a/redisinsight/ui/src/mocks/handlers/index.ts b/redisinsight/ui/src/mocks/handlers/index.ts index be3438003c..d4321d2112 100644 --- a/redisinsight/ui/src/mocks/handlers/index.ts +++ b/redisinsight/ui/src/mocks/handlers/index.ts @@ -5,6 +5,7 @@ import app from './app' import analytics from './analytics' import browser from './browser' import recommendations from './recommendations' +import triggeredFunctions from './triggeredFunctions' // @ts-ignore export const handlers: RestHandler[] = [].concat( @@ -14,4 +15,5 @@ export const handlers: RestHandler[] = [].concat( analytics, browser, recommendations, + triggeredFunctions, ) diff --git a/redisinsight/ui/src/mocks/handlers/triggeredFunctions/index.ts b/redisinsight/ui/src/mocks/handlers/triggeredFunctions/index.ts new file mode 100644 index 0000000000..fc9e465b91 --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/triggeredFunctions/index.ts @@ -0,0 +1,6 @@ +import { DefaultBodyType, MockedRequest, RestHandler } from 'msw' + +import triggeredFunctions from './triggeredFunctionsHandler' + +const handlers: RestHandler>[] = [].concat(triggeredFunctions) +export default handlers diff --git a/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts b/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts new file mode 100644 index 0000000000..dfbff35cb9 --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts @@ -0,0 +1,33 @@ +import { rest, RestHandler } from 'msw' +import { getMswURL } from 'uiSrc/utils/test-utils' +import { getUrl } from 'uiSrc/utils' +import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers' +import { ApiEndpoints } from 'uiSrc/constants' + +export const TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA = { + apiVersion: '1.2', + code: 'code', + configuration: 'config', + functions: [ + { name: 'foo', type: 'functions' }, + { name: 'foo1', type: 'functions' }, + { name: 'foo2', type: 'cluster_functions' }, + { name: 'foo3', type: 'keyspace_triggers' }, + ], + name: 'lib', + pendingJobs: 12, + user: 'default', +} + +const handlers: RestHandler[] = [ + // fetch triggered functions lib details + rest.post(getMswURL( + getUrl(INSTANCE_ID_MOCK, ApiEndpoints.TRIGGERED_FUNCTIONS_GET_LIBRARY) + ), + async (req, res, ctx) => res( + ctx.status(200), + ctx.json(TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA), + )), +] + +export default handlers diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.tsx index dfb1bb8f36..fb958f0733 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.tsx @@ -14,7 +14,7 @@ import { import { Maybe, stringToBuffer } from 'uiSrc/utils' import { addKeyStateSelector, addReJSONKey, } from 'uiSrc/slices/browser/keys' -import MonacoJson from 'uiSrc/components/monaco-json' +import { MonacoJson } from 'uiSrc/components/monaco-editor' import UploadFile from 'uiSrc/components/upload-file' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { CreateRejsonRlWithExpireDto } from 'apiSrc/modules/browser/dto' diff --git a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx b/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx index be03a12a67..ab02e79893 100644 --- a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx +++ b/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx @@ -30,7 +30,7 @@ export interface Props { containerClassName?: string turnOffAutoRefresh?: boolean onRefresh: (enableAutoRefresh: boolean) => void - onRefreshClicked: () => void + onRefreshClicked?: () => void onEnableAutoRefresh?: (enableAutoRefresh: boolean, refreshRate: string) => void onChangeAutoRefreshRate?: (enableAutoRefresh: boolean, refreshRate: string) => void } diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx index 45ab1cae36..0be6953a40 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx @@ -2,7 +2,7 @@ import React from 'react' import { cloneDeep } from 'lodash' import { cleanup, mockedStore, render, screen, fireEvent } from 'uiSrc/utils/test-utils' -import { getTriggeredFunctionsList, triggeredFunctionsSelector } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { getTriggeredFunctionsLibrariesList, triggeredFunctionsSelector } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import LibrariesPage from './LibrariesPage' jest.mock('uiSrc/slices/triggeredFunctions/triggeredFunctions', () => ({ @@ -49,7 +49,7 @@ describe('LibrariesPage', () => { it('should fetch list of libraries', () => { render() - const expectedActions = [getTriggeredFunctionsList()] + const expectedActions = [getTriggeredFunctionsLibrariesList()] expect(store.getActions()).toEqual(expectedActions) }) @@ -103,4 +103,16 @@ describe('LibrariesPage', () => { ) expect(screen.queryAllByTestId(/^row-/).length).toEqual(3) }) + + it('should open library details', () => { + (triggeredFunctionsSelector as jest.Mock).mockReturnValueOnce({ + libraries: mockedLibraries, + loading: false + }) + render() + + fireEvent.click(screen.getByTestId('row-lib1')) + + expect(screen.getByTestId('lib-details-lib1')).toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx index 01b4ac6806..52c096f041 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx @@ -5,24 +5,33 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, + EuiResizableContainer, } from '@elastic/eui' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' +import cx from 'classnames' import { fetchTriggeredFunctionsLibrariesList, + setTriggeredFunctionsSelectedLibrary, triggeredFunctionsSelector } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { TriggeredFunctionsLibrary } from 'uiSrc/slices/interfaces/triggeredFunctions' +import { Nullable } from 'uiSrc/utils' import NoLibrariesScreen from './components/NoLibrariesScreen' import LibrariesList from './components/LibrariesList' +import LibraryDetails from './components/LibraryDetails' import styles from './styles.module.scss' +export const firstPanelId = 'libraries-left-panel' +export const secondPanelId = 'libraries-right-panel' + const LibrariesPage = () => { const { lastRefresh, loading, libraries } = useSelector(triggeredFunctionsSelector) const [items, setItems] = useState([]) const [filterValue, setFilterValue] = useState('') + const [selectedRow, setSelectedRow] = useState>(null) const { instanceId } = useParams<{ instanceId: string }>() const dispatch = useDispatch() @@ -43,6 +52,15 @@ const LibrariesPage = () => { setFilterValue(e.target.value.toLowerCase()) } + const handleSelectRow = (name?: string) => { + setSelectedRow(name ?? null) + + if (name !== selectedRow) { + // clear prev value to get new one + dispatch(setTriggeredFunctionsSelectedLibrary(null)) + } + } + const applyFiltering = () => { if (!filterValue) { setItems(libraries || []) @@ -67,7 +85,7 @@ const LibrariesPage = () => { className={styles.topPanel} > - {libraries?.length > 0 && ( + {!!libraries?.length && ( { - {!libraries && loading && ( -
- -
- )} - {(libraries?.length > 0) && ( - - )} - {libraries?.length === 0 && ( - - )} + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + +
+ {!libraries && loading && ( +
+ +
+ )} + {!!libraries?.length && ( + + )} + {libraries?.length === 0 && ( + + )} +
+
+ + +
+ {selectedRow && ( + + )} +
+
+ + )} +
) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx index 37d538c3a7..b6319dd6b6 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx @@ -14,14 +14,15 @@ export interface Props { loading: boolean onRefresh: () => void lastRefresh: Nullable + selectedRow: Nullable + onSelectRow: (name: string) => void } const NoLibrariesMessage: React.ReactNode = (No Libraries found) const LibrariesList = (props: Props) => { - const { items, loading, onRefresh, lastRefresh } = props + const { items, loading, onRefresh, lastRefresh, selectedRow, onSelectRow } = props const [sort, setSort] = useState>(undefined) - const [selectedRow, setSelectedRow] = useState>(null) const { instanceId } = useParams<{ instanceId: string }>() @@ -78,7 +79,7 @@ const LibrariesList = (props: Props) => { ] const handleSelect = (item: TriggeredFunctionsLibrary) => { - setSelectedRow(item.name) + onSelectRow(item.name) } const handleRefreshClicked = () => { diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.spec.tsx new file mode 100644 index 0000000000..60cadfff2f --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.spec.tsx @@ -0,0 +1,142 @@ +import React from 'react' +import { cloneDeep } from 'lodash' +import { cleanup, mockedStore, render, screen, fireEvent, act } from 'uiSrc/utils/test-utils' + +import { + getTriggeredFunctionsLibraryDetails, + triggeredFunctionsSelectedLibrarySelector +} from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' + +import { + TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA +} from 'uiSrc/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler' +import LibraryDetails from './LibraryDetails' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +jest.mock('uiSrc/slices/triggeredFunctions/triggeredFunctions', () => ({ + ...jest.requireActual('uiSrc/slices/triggeredFunctions/triggeredFunctions'), + triggeredFunctionsSelectedLibrarySelector: jest.fn().mockReturnValue({ + ...jest.requireActual('uiSrc/slices/triggeredFunctions/triggeredFunctions').triggeredFunctionsSelectedLibrarySelector + }) +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('LibraryDetails', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call fetch details on render', () => { + render() + + const expectedActions = [getTriggeredFunctionsLibraryDetails()] + expect(store.getActions()).toEqual(expectedActions) + }) + + it('should call onCLose', () => { + const onClose = jest.fn() + render() + + fireEvent.click(screen.getByTestId('close-right-panel-btn')) + expect(onClose).toBeCalled() + }) + + it('should render proper content', async () => { + (triggeredFunctionsSelectedLibrarySelector as jest.Mock).mockReturnValueOnce({ + lastRefresh: null, + loading: false, + data: { + apiVersion: '1.2', + code: 'code', + configuration: 'config', + functions: [ + { name: 'foo', type: 'functions' }, + { name: 'foo1', type: 'functions' }, + { name: 'foo2', type: 'cluster_functions' }, + { name: 'foo3', type: 'keyspace_triggers' }, + ], + name: 'lib', + pendingJobs: 12, + user: 'default', + } + }) + + render() + + expect(screen.getByTestId('lib-name')).toHaveTextContent('lib') + expect(screen.getByTestId('lib-apiVersion')).toHaveTextContent('1.2') + expect(screen.getByTestId('functions-Functions')).toHaveTextContent('Functions (2)foofoo1') + expect(screen.getByTestId('functions-Keyspace triggers')).toHaveTextContent('Keyspace triggers (1)foo3') + expect(screen.getByTestId('functions-Cluster Functions')).toHaveTextContent('Cluster Functions (1)foo2') + expect(screen.getByTestId('functions-Stream Functions')).toHaveTextContent('Stream Functions Empty') + + expect(screen.getByTestId('library-code')).toHaveValue('code') + + fireEvent.click(screen.getByTestId('library-view-tab-config')) + expect(screen.getByTestId('library-configuration')).toHaveValue('config') + }) + + it('should call proper telemetry events', async () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + await act(() => { + render() + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_VIEWED, + eventData: { + databaseId: 'instanceId', + apiVersion: TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA.apiVersion, + pendingJobs: TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA.pendingJobs + } + }) + + fireEvent.click(screen.getByTestId('refresh-lib-details-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_REFRESH_CLICKED, + eventData: { + databaseId: 'instanceId' + } + }) + + sendEventTelemetry.mockRestore() + + fireEvent.click(screen.getByTestId('auto-refresh-config-btn')) + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_ENABLED, + eventData: { + refreshRate: '5.0', + databaseId: 'instanceId' + } + }) + + sendEventTelemetry.mockRestore() + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_DISABLED, + eventData: { + refreshRate: '5.0', + databaseId: 'instanceId' + } + }) + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx new file mode 100644 index 0000000000..b9a42b1683 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx @@ -0,0 +1,271 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiToolTip, + EuiSpacer, + EuiText, + EuiCollapsibleNavGroup, + EuiLoadingContent, + EuiTabs, + EuiTab, + EuiProgress +} from '@elastic/eui' +import cx from 'classnames' +import { + fetchTriggeredFunctionsLibrary, + replaceTriggeredFunctionsLibraryAction, + triggeredFunctionsSelectedLibrarySelector +} from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' + +import { MonacoJS, MonacoJson } from 'uiSrc/components/monaco-editor' +import { reSerializeJSON } from 'uiSrc/utils/formatters/json' +import { FunctionType } from 'uiSrc/slices/interfaces/triggeredFunctions' + +import AutoRefresh from 'uiSrc/pages/browser/components/auto-refresh' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' + +import styles from './styles.module.scss' + +export interface Props { + name: string + onClose: () => void +} + +const LIST_OF_FUNCTION_TYPES = [ + { title: 'Functions', type: FunctionType.Function }, + { title: 'Keyspace triggers', type: FunctionType.KeyspaceTrigger }, + { title: 'Cluster Functions', type: FunctionType.ClusterFunction }, + { title: 'Stream Functions', type: FunctionType.StreamTrigger }, +] + +enum SelectedView { + Code = 'code', + Config = 'config' +} + +const tabs = [ + { id: SelectedView.Code, label: 'Library Code' }, + { id: SelectedView.Config, label: 'Configuration' } +] + +const LibraryDetails = (props: Props) => { + const { name, onClose } = props + const { loading, lastRefresh, data: library } = useSelector(triggeredFunctionsSelectedLibrarySelector) + + const [selectedView, setSelectedView] = useState(tabs[0].id) + const [configuration, setConfiguration] = useState('') + const [code, setCode] = useState('') + + const { instanceId } = useParams<{ instanceId: string }>() + const dispatch = useDispatch() + + useEffect(() => { + dispatch(fetchTriggeredFunctionsLibrary( + instanceId, + name, + (lib) => { + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_VIEWED, + eventData: { + databaseId: instanceId, + pendingJobs: lib?.pendingJobs || 0, + apiVersion: lib?.apiVersion || '1.0' + } + }) + } + )) + }, [name]) + + useEffect(() => { + setConfiguration(reSerializeJSON(library?.configuration ?? '', 2)) + setCode(library?.code ?? '') + }, [library]) + + const handleRefresh = () => { + dispatch(fetchTriggeredFunctionsLibrary(instanceId, name)) + } + + const handleRefreshClicked = () => { + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_REFRESH_CLICKED, + eventData: { + databaseId: instanceId, + } + }) + } + + const handleEnableAutoRefresh = (enableAutoRefresh: boolean, refreshRate: string) => { + sendEventTelemetry({ + event: enableAutoRefresh + ? TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_ENABLED + : TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_DISABLED, + eventData: { + databaseId: instanceId, + refreshRate + } + }) + } + + const handleApply = (_e: React.MouseEvent, closeEditor: () => void) => { + dispatch(replaceTriggeredFunctionsLibraryAction(instanceId, code, configuration, () => { + closeEditor() + sendEventTelemetry({ + event: selectedView === SelectedView.Code + ? TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_CODE_REPLACED + : TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_CONFIGURATION_REPLACED, + eventData: { + databaseId: instanceId, + } + }) + })) + } + + const handleDecline = () => { + setConfiguration(reSerializeJSON(library?.configuration ?? '', 2)) + setCode(library?.code ?? '') + } + + const functionGroup = (title: string, list: Array<{ type: FunctionType, name: string }>, initialIsOpen = false) => { + const count = list.length > 0 ? `(${list.length})` : '' + return ( + + {list.length ? ( +
    + {list.map(({ name }) => ( +
  • + {name} +
  • + ))} +
+ ) : ( + Empty + )} +
+ ) + } + + const renderFunctionsLists = () => LIST_OF_FUNCTION_TYPES.map(({ title, type }) => { + const functionsList = library?.functions?.filter(({ type: fType }: { type: FunctionType }) => type === fType) || [] + return functionGroup(title, functionsList, functionsList.length > 0) + }) + + const renderTabs = useCallback(() => tabs.map(({ id, label }) => ( + setSelectedView(id)} + key={id} + data-testid={`library-view-tab-${id}`} + className={styles.tab} + > + {label} + + )), [selectedView]) + + return ( +
+
+ + {name} + + + + + {library?.apiVersion && (API: {library.apiVersion})} + + + + + + + onClose()} + data-testid="close-right-panel-btn" + /> + +
+
+ {(loading && library) && ( + + )} + {(loading && !library) && ()} + {library && ( + <> + {renderFunctionsLists()} + {renderTabs()} + {selectedView === SelectedView.Code && ( + + )} + {selectedView === SelectedView.Config && ( + + )} + + )} +
+
+ ) +} + +export default LibraryDetails diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/index.ts new file mode 100644 index 0000000000..333a341b9f --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/index.ts @@ -0,0 +1,3 @@ +import LibraryDetails from './LibraryDetails' + +export default LibraryDetails diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/styles.module.scss new file mode 100644 index 0000000000..695a5da7c3 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/styles.module.scss @@ -0,0 +1,87 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/components/table/mixins"; +@import "@elastic/eui/src/global_styling/index"; + +.main { + position: relative; + height: 100%; + + .header { + height: 92px; + padding: 16px; + border-bottom: 1px solid var(--euiColorLightShade); + + .titleTooltip { + width: auto; + max-width: calc(100% - 80px); + } + } + + .content { + position: relative; + padding: 16px 16px 40px; + + @include euiScrollBar; + overflow: auto; + height: 100%; + max-height: calc(100% - 92px); + } + + .editorWrapper { + height: 280px; + + :global(.inlineMonacoEditor) { + height: 278px; + } + } + + .closeRightPanel { + position: absolute; + top: 16px; + right: 16px; + } + + .accordion { + margin-top: 0; + margin-bottom: 20px; + + &:global(.euiAccordion-isOpen) { + :global { + .euiAccordion__triggerWrapper { + border-radius: 4px 4px 0 0; + background: var(--euiColorLightestShade); + } + } + } + + :global { + .euiAccordion__triggerWrapper { + padding: 6px 10px; + background: transparent; + border: 1px solid var(--controlsBorderColor); + border-radius: 4px; + } + + .euiAccordion__childWrapper { + background: transparent; + border: 1px solid var(--controlsBorderColor) !important; + border-top-width: 0 !important; + border-radius: 0 0 4px 4px; + } + + .euiCollapsibleNavGroup__title { + color: var(--euiTextSubduedColor) !important; + font-size: 14px !important; + font-weight: 400 !important; + } + } + } + + .listItem { + margin-bottom: 4px; + } + + :global(.monaco-editor.readMode .monaco-mouse-cursor-text) { + cursor: default; + } +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss index 99e201954b..06db8386ea 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/styles.module.scss @@ -1,3 +1,5 @@ +$breakpoint-to-hide-resize-panel: 1124px; + .topPanel { margin: 16px 0; min-height: 40px; @@ -17,9 +19,55 @@ } } +.resizePanelLeft { + padding-right: 8px; -.main { + &.withSelectedRow { + @media screen and (max-width: $breakpoint-to-hide-resize-panel) { + display: none; + } + } +} + +.resizableButton { + @media screen and (max-width: $breakpoint-to-hide-resize-panel) { + display: none; + } +} + +.resizePanelRight { + padding-left: 8px; + + @media screen and (max-width: $breakpoint-to-hide-resize-panel) { + padding-left: 0; + width: 100% !important; + } +} + +.panelWrapper { background-color: var(--euiColorEmptyShade); + height: 100%; +} + +.hidden { + display: none; +} + +.noVisible { + visibility: hidden; +} + +.fullWidth { + width: 100% !important; + min-width: 100% !important; + padding: 0; + :global(.euiResizablePanel__content) { + padding-right: 0; + padding-left: 0; + } +} + +.main { max-height: calc(100% - 72px); .loading { diff --git a/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts b/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts index a2eb3af0dc..9f671defd8 100644 --- a/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts +++ b/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts @@ -1,10 +1,23 @@ import { Nullable } from 'uiSrc/utils' -export interface TriggeredFunctionsFunctions { - flags: string[] - isAsync: boolean +export enum FunctionType { + Function = 'functions', + ClusterFunction = 'cluster_functions', + KeyspaceTrigger = 'keyspace_triggers', + StreamTrigger = 'stream_triggers', +} + +export interface TriggeredFunctionsLibraryDetails { + apiVersion: string + code: string + configuration: Nullable + functions: Array<{ + type: FunctionType + name: string + }> name: string - type: string + pendingJobs: number + user: string } export interface TriggeredFunctionsLibrary { @@ -16,6 +29,11 @@ export interface TriggeredFunctionsLibrary { export interface StateTriggeredFunctions { libraries: Nullable + selectedLibrary: { + lastRefresh: Nullable + data: Nullable + loading: boolean + } loading: boolean, lastRefresh: Nullable error: string diff --git a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts index 9d7cb39190..33da221fc2 100644 --- a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts +++ b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts @@ -3,14 +3,25 @@ import { AxiosError } from 'axios' import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' import reducer, { fetchTriggeredFunctionsLibrariesList, - getTriggeredFunctionsFailure, - getTriggeredFunctionsList, - getTriggeredFunctionsListSuccess, + getTriggeredFunctionsLibrariesListFailure, + getTriggeredFunctionsLibrariesList, + getTriggeredFunctionsLibrariesListSuccess, initialState, - triggeredFunctionsSelector + triggeredFunctionsSelector, + getTriggeredFunctionsLibraryDetails, + getTriggeredFunctionsLibraryDetailsSuccess, + getTriggeredFunctionsLibraryDetailsFailure, + replaceTriggeredFunctionsLibrary, + replaceTriggeredFunctionsLibrarySuccess, + replaceTriggeredFunctionsLibraryFailure, + fetchTriggeredFunctionsLibrary, + replaceTriggeredFunctionsLibraryAction } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { apiService } from 'uiSrc/services' import { addErrorNotification } from 'uiSrc/slices/app/notifications' +import { + TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA +} from 'uiSrc/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler' let store: typeof mockedStore @@ -45,7 +56,7 @@ describe('triggeredFunctions slice', () => { }) }) - describe('getTriggeredFunctionsList', () => { + describe('getTriggeredFunctionsLibrariesList', () => { it('should properly set state', () => { // Arrange const state = { @@ -54,7 +65,7 @@ describe('triggeredFunctions slice', () => { } // Act - const nextState = reducer(initialState, getTriggeredFunctionsList()) + const nextState = reducer(initialState, getTriggeredFunctionsLibrariesList()) // Assert const rootState = Object.assign(initialStateDefault, { @@ -64,7 +75,7 @@ describe('triggeredFunctions slice', () => { }) }) - describe('getTriggeredFunctionsListSuccess', () => { + describe('getTriggeredFunctionsLibrariesListSuccess', () => { it('should properly set state', () => { // Arrange const libraries = [{ name: 'lib1', user: 'user1' }] @@ -75,7 +86,7 @@ describe('triggeredFunctions slice', () => { } // Act - const nextState = reducer(initialState, getTriggeredFunctionsListSuccess(libraries)) + const nextState = reducer(initialState, getTriggeredFunctionsLibrariesListSuccess(libraries)) // Assert const rootState = Object.assign(initialStateDefault, { @@ -85,7 +96,7 @@ describe('triggeredFunctions slice', () => { }) }) - describe('getTriggeredFunctionsFailure', () => { + describe('getTriggeredFunctionsLibrariesListFailure', () => { it('should properly set state', () => { // Arrange const error = 'error' @@ -95,7 +106,162 @@ describe('triggeredFunctions slice', () => { } // Act - const nextState = reducer(initialState, getTriggeredFunctionsFailure(error)) + const nextState = reducer(initialState, getTriggeredFunctionsLibrariesListFailure(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('getTriggeredFunctionsLibraryDetails', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + selectedLibrary: { + ...initialState.selectedLibrary, + loading: true + } + } + + // Act + const nextState = reducer(initialState, getTriggeredFunctionsLibraryDetails()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('getTriggeredFunctionsLibraryDetailsSuccess', () => { + it('should properly set state', () => { + // Arrange + const libraryDetails = TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA + const state = { + ...initialState, + selectedLibrary: { + ...initialState.selectedLibrary, + lastRefresh: Date.now(), + data: libraryDetails + } + } + + // Act + const nextState = reducer(initialState, getTriggeredFunctionsLibraryDetailsSuccess(libraryDetails)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('getTriggeredFunctionsLibraryDetailsFailure', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + selectedLibrary: { + ...initialState.selectedLibrary, + loading: true + } + } + const state = { + ...initialState, + selectedLibrary: { + ...initialState.selectedLibrary, + loading: false + }, + } + + // Act + const nextState = reducer(currentState, getTriggeredFunctionsLibraryDetailsFailure()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('replaceTriggeredFunctionsLibrary', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + selectedLibrary: { + ...initialState.selectedLibrary, + loading: true + } + } + + // Act + const nextState = reducer(initialState, replaceTriggeredFunctionsLibrary()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('replaceTriggeredFunctionsLibrarySuccess', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + selectedLibrary: { + ...initialState.selectedLibrary, + loading: true + } + } + const state = { + ...initialState, + selectedLibrary: { + ...initialState.selectedLibrary, + loading: false, + }, + } + + // Act + const nextState = reducer(currentState, replaceTriggeredFunctionsLibrarySuccess()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('replaceTriggeredFunctionsLibraryFailure', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + selectedLibrary: { + ...initialState.selectedLibrary, + loading: true + } + } + const state = { + ...initialState, + selectedLibrary: { + ...initialState.selectedLibrary, + loading: false + }, + } + + // Act + const nextState = reducer(currentState, replaceTriggeredFunctionsLibraryFailure()) // Assert const rootState = Object.assign(initialStateDefault, { @@ -122,8 +288,8 @@ describe('triggeredFunctions slice', () => { // Assert const expectedActions = [ - getTriggeredFunctionsList(), - getTriggeredFunctionsListSuccess(data), + getTriggeredFunctionsLibrariesList(), + getTriggeredFunctionsLibrariesListSuccess(data), ] expect(store.getActions()).toEqual(expectedActions) @@ -147,9 +313,104 @@ describe('triggeredFunctions slice', () => { // Assert const expectedActions = [ - getTriggeredFunctionsList(), + getTriggeredFunctionsLibrariesList(), + addErrorNotification(responsePayload as AxiosError), + getTriggeredFunctionsLibrariesListFailure(errorMessage) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) + + describe('fetchTriggeredFunctionsLibrary', () => { + it('succeed to fetch data', async () => { + const data = TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA + const responsePayload = { data, status: 200 } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch( + fetchTriggeredFunctionsLibrary('123', 'lib') + ) + + // Assert + const expectedActions = [ + getTriggeredFunctionsLibraryDetails(), + getTriggeredFunctionsLibraryDetailsSuccess(data), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch data', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch( + fetchTriggeredFunctionsLibrary('123', 'lib') + ) + + // Assert + const expectedActions = [ + getTriggeredFunctionsLibraryDetails(), + addErrorNotification(responsePayload as AxiosError), + getTriggeredFunctionsLibraryDetailsFailure() + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) + + describe('replaceTriggeredFunctionsLibraryAction', () => { + it('succeed to fetch data', async () => { + const responsePayload = { status: 200 } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch( + replaceTriggeredFunctionsLibraryAction('123', 'code', 'config') + ) + + // Assert + const expectedActions = [ + replaceTriggeredFunctionsLibrary(), + replaceTriggeredFunctionsLibrarySuccess(), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch data', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch( + replaceTriggeredFunctionsLibraryAction('123', 'code', 'config') + ) + + // Assert + const expectedActions = [ + replaceTriggeredFunctionsLibrary(), addErrorNotification(responsePayload as AxiosError), - getTriggeredFunctionsFailure(errorMessage) + replaceTriggeredFunctionsLibraryFailure() ] expect(store.getActions()).toEqual(expectedActions) diff --git a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts index b92e3f43e6..cb611b3a14 100644 --- a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts +++ b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts @@ -1,14 +1,19 @@ import { createSlice } from '@reduxjs/toolkit' import { AxiosError } from 'axios' -import { StateTriggeredFunctions } from 'uiSrc/slices/interfaces/triggeredFunctions' +import { StateTriggeredFunctions, TriggeredFunctionsLibraryDetails } from 'uiSrc/slices/interfaces/triggeredFunctions' import { AppDispatch, RootState } from 'uiSrc/slices/store' import { apiService } from 'uiSrc/services' -import { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils' +import { getApiErrorMessage, getUrl, isStatusSuccessful, Nullable } from 'uiSrc/utils' import { ApiEndpoints } from 'uiSrc/constants' import { addErrorNotification } from 'uiSrc/slices/app/notifications' export const initialState: StateTriggeredFunctions = { libraries: null, + selectedLibrary: { + lastRefresh: null, + loading: false, + data: null + }, loading: false, lastRefresh: null, error: '', @@ -19,30 +24,61 @@ const triggeredFunctionsSlice = createSlice({ initialState, reducers: { setTriggeredFunctionsInitialState: () => initialState, - getTriggeredFunctionsList: (state) => { + getTriggeredFunctionsLibrariesList: (state) => { state.loading = true state.error = '' }, - getTriggeredFunctionsListSuccess: (state, { payload }) => { + getTriggeredFunctionsLibrariesListSuccess: (state, { payload }) => { state.loading = false state.lastRefresh = Date.now() state.libraries = payload }, - getTriggeredFunctionsFailure: (state, { payload }) => { + getTriggeredFunctionsLibrariesListFailure: (state, { payload }) => { state.loading = false state.error = payload + }, + setTriggeredFunctionsSelectedLibrary: (state, { payload }) => { + state.selectedLibrary.data = payload + }, + getTriggeredFunctionsLibraryDetails: (state) => { + state.selectedLibrary.loading = true + }, + getTriggeredFunctionsLibraryDetailsSuccess: (state, { payload }) => { + state.selectedLibrary.loading = false + state.selectedLibrary.lastRefresh = Date.now() + state.selectedLibrary.data = payload + }, + getTriggeredFunctionsLibraryDetailsFailure: (state) => { + state.selectedLibrary.loading = false + }, + replaceTriggeredFunctionsLibrary: (state) => { + state.selectedLibrary.loading = true + }, + replaceTriggeredFunctionsLibrarySuccess: (state) => { + state.selectedLibrary.loading = false + }, + replaceTriggeredFunctionsLibraryFailure: (state) => { + state.selectedLibrary.loading = false } } }) export const { setTriggeredFunctionsInitialState, - getTriggeredFunctionsList, - getTriggeredFunctionsListSuccess, - getTriggeredFunctionsFailure, + getTriggeredFunctionsLibrariesList, + getTriggeredFunctionsLibrariesListSuccess, + getTriggeredFunctionsLibrariesListFailure, + setTriggeredFunctionsSelectedLibrary, + getTriggeredFunctionsLibraryDetails, + getTriggeredFunctionsLibraryDetailsSuccess, + getTriggeredFunctionsLibraryDetailsFailure, + replaceTriggeredFunctionsLibrary, + replaceTriggeredFunctionsLibrarySuccess, + replaceTriggeredFunctionsLibraryFailure, } = triggeredFunctionsSlice.actions export const triggeredFunctionsSelector = (state: RootState) => state.triggeredFunctions +export const triggeredFunctionsSelectedLibrarySelector = (state: RootState) => state.triggeredFunctions.selectedLibrary export default triggeredFunctionsSlice.reducer @@ -54,7 +90,7 @@ export function fetchTriggeredFunctionsLibrariesList( ) { return async (dispatch: AppDispatch) => { try { - dispatch(getTriggeredFunctionsList()) + dispatch(getTriggeredFunctionsLibrariesList()) const { data, status } = await apiService.get( getUrl( @@ -64,14 +100,82 @@ export function fetchTriggeredFunctionsLibrariesList( ) if (isStatusSuccessful(status)) { - dispatch(getTriggeredFunctionsListSuccess(data)) + dispatch(getTriggeredFunctionsLibrariesListSuccess(data)) onSuccessAction?.() } } catch (_err) { const error = _err as AxiosError const errorMessage = getApiErrorMessage(error) dispatch(addErrorNotification(error)) - dispatch(getTriggeredFunctionsFailure(errorMessage)) + dispatch(getTriggeredFunctionsLibrariesListFailure(errorMessage)) + onFailAction?.() + } + } +} + +export function fetchTriggeredFunctionsLibrary( + instanceId: string, + libName: string, + onSuccessAction?: (data: TriggeredFunctionsLibraryDetails) => void, + onFailAction?: () => void, +) { + return async (dispatch: AppDispatch) => { + try { + dispatch(getTriggeredFunctionsLibraryDetails()) + + const { data, status } = await apiService.post( + getUrl( + instanceId, + ApiEndpoints.TRIGGERED_FUNCTIONS_GET_LIBRARY, + ), + { + libraryName: libName + } + ) + + if (isStatusSuccessful(status)) { + dispatch(getTriggeredFunctionsLibraryDetailsSuccess(data)) + onSuccessAction?.(data) + } + } catch (_err) { + const error = _err as AxiosError + dispatch(addErrorNotification(error)) + dispatch(getTriggeredFunctionsLibraryDetailsFailure()) + onFailAction?.() + } + } +} + +export function replaceTriggeredFunctionsLibraryAction( + instanceId: string, + code: string, + configuration: Nullable, + onSuccessAction?: () => void, + onFailAction?: () => void, +) { + return async (dispatch: AppDispatch) => { + try { + dispatch(replaceTriggeredFunctionsLibrary()) + + const { status } = await apiService.post( + getUrl( + instanceId, + ApiEndpoints.TRIGGERED_FUNCTIONS_REPLACE_LIBRARY, + ), + { + code, + configuration + } + ) + + if (isStatusSuccessful(status)) { + dispatch(replaceTriggeredFunctionsLibrarySuccess()) + onSuccessAction?.() + } + } catch (_err) { + const error = _err as AxiosError + dispatch(addErrorNotification(error)) + dispatch(replaceTriggeredFunctionsLibraryFailure()) onFailAction?.() } } diff --git a/redisinsight/ui/src/styles/components/_tabs.scss b/redisinsight/ui/src/styles/components/_tabs.scss index 166250e16e..9d85ffc4f2 100644 --- a/redisinsight/ui/src/styles/components/_tabs.scss +++ b/redisinsight/ui/src/styles/components/_tabs.scss @@ -42,7 +42,7 @@ content: " "; height: 18px; left: -10px; - width: 2px; + width: 1px; } } diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index c58c69f7ca..8419f28e40 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -239,5 +239,11 @@ export enum TelemetryEvent { TRIGGERS_AND_FUNCTIONS_LIBRARIES_SORTED = 'TRIGGERS_AND_FUNCTIONS_LIBRARIES_SORTED', TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_REFRESH_CLICKED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_REFRESH_CLICKED', TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_ENABLED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_ENABLED', - TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED' + TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_VIEWED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_VIEWED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_REFRESH_CLICKED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_REFRESH_CLICKED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_ENABLED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_ENABLED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_DISABLED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_DISABLED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_CODE_REPLACED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_CODE_REPLACED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_CONFIGURATION_REPLACED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_CONFIGURATION_REPLACED' } From 765d723bc5127f6bfce4417acf51e64df1b84dfe Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 20 Jun 2023 17:18:10 +0200 Subject: [PATCH 018/106] #RI-4591 - change logic to clear selection --- .../components/monaco-editor/MonacoEditor.tsx | 11 ----------- .../components/LibraryDetails/LibraryDetails.tsx | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx index 7694add1e1..c3bdeca3b6 100644 --- a/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx +++ b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx @@ -52,7 +52,6 @@ const MonacoEditor = (props: Props) => { const [isEditing, setIsEditing] = useState(!readOnly && !disabled) const monacoObjects = useRef>(null) - const valueRef = useRef('') const { theme } = useContext(ThemeContext) @@ -60,16 +59,6 @@ const MonacoEditor = (props: Props) => { monacoObjects.current?.editor.updateOptions({ readOnly: !isEditing && (disabled || readOnly) }) }, [disabled, readOnly, isEditing]) - useEffect(() => { - // clear selection after update empty value - // https://github.com/react-monaco-editor/react-monaco-editor/issues/539 - if (value && !valueRef.current) { - monacoObjects.current?.editor.setSelection(new monaco.Selection(0, 0, 0, 0)) - } - - valueRef.current = value - }, [value]) - const editorDidMount = ( editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: typeof monacoEditor, diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx index b9a42b1683..e10f386985 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx @@ -58,8 +58,8 @@ const LibraryDetails = (props: Props) => { const { loading, lastRefresh, data: library } = useSelector(triggeredFunctionsSelectedLibrarySelector) const [selectedView, setSelectedView] = useState(tabs[0].id) - const [configuration, setConfiguration] = useState('') - const [code, setCode] = useState('') + const [configuration, setConfiguration] = useState('_') + const [code, setCode] = useState('_') const { instanceId } = useParams<{ instanceId: string }>() const dispatch = useDispatch() @@ -82,8 +82,16 @@ const LibraryDetails = (props: Props) => { }, [name]) useEffect(() => { - setConfiguration(reSerializeJSON(library?.configuration ?? '', 2)) - setCode(library?.code ?? '') + // set first values, to clear selection after lib will be fetched + // https://github.com/react-monaco-editor/react-monaco-editor/issues/539 + if (!library) { + setCode('_') + setConfiguration('_') + return + } + + setConfiguration(reSerializeJSON(library.configuration ?? '', 2)) + setCode(library.code) }, [library]) const handleRefresh = () => { From 13fcf09d619d20b5a19b1f9a172e77f6f09551c5 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 21 Jun 2023 14:33:00 +0200 Subject: [PATCH 019/106] #RI-4663 - fix scroll #RI-4664 - fix styles for long list --- .../Libraries/components/LibrariesList/styles.module.scss | 3 ++- .../Libraries/components/LibraryDetails/styles.module.scss | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss index a590ecd8f3..3d5c1d8427 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss @@ -4,6 +4,8 @@ .tableWrapper { max-height: 100%; + display: flex; + flex-direction: column; .header { height: 42px; @@ -19,7 +21,6 @@ @include euiScrollBar; overflow: auto; position: relative; - max-height: calc(100% - 42px); :global { thead { diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/styles.module.scss index 695a5da7c3..d3a06cb05c 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/styles.module.scss +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/styles.module.scss @@ -74,11 +74,18 @@ font-size: 14px !important; font-weight: 400 !important; } + + .euiCollapsibleNavGroup__children { + max-height: 200px; + overflow-y: auto; + @include euiScrollBar; + } } } .listItem { margin-bottom: 4px; + word-wrap: break-word; } :global(.monaco-editor.readMode .monaco-mouse-cursor-text) { From ea468bc335dc2b7a3cb49714f72a6a2d67c55b7a Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 26 Jun 2023 15:42:07 +0200 Subject: [PATCH 020/106] #RI-4581 - add functions page to triggers & functions --- .../main-router/constants/redisStackRoutes.ts | 4 +- .../sub-routes/triggeredFunctionsRoutes.ts | 4 +- redisinsight/ui/src/constants/api.ts | 1 + .../TriggeredFunctionsPage.tsx | 31 ++- .../TriggeredFunctionsTabs.tsx | 57 ++++++ .../TriggeredFunctionsTabs/index.ts | 3 + .../src/pages/triggeredFunctions/constants.ts | 49 +++++ .../pages/Functions/FunctionsPage.tsx | 187 ++++++++++++++++++ .../FunctionDetails/FunctionDetails.spec.tsx | 142 +++++++++++++ .../FunctionDetails/FunctionDetails.tsx | 155 +++++++++++++++ .../components/FunctionDetails/index.ts | 3 + .../FunctionDetails/styles.module.scss | 64 ++++++ .../FunctionsList/FunctionsList.spec.tsx | 89 +++++++++ .../FunctionsList/FunctionsList.tsx | 138 +++++++++++++ .../components/FunctionsList/index.ts | 3 + .../FunctionsList/styles.module.scss | 37 ++++ .../pages/Functions/index.ts | 3 + .../pages/Functions/styles.module.scss | 19 ++ .../pages/Libraries/LibrariesPage.tsx | 59 ++++-- .../LibrariesList/LibrariesList.tsx | 6 +- .../LibraryDetails/LibraryDetails.tsx | 78 +++++--- .../LibraryDetails/styles.module.scss | 51 +---- .../pages/Libraries/styles.module.scss | 70 ------- .../pages/triggeredFunctions/pages/index.ts | 4 +- .../triggeredFunctions/styles.modules.scss | 162 +++++++++++++++ .../slices/interfaces/triggeredFunctions.ts | 36 +++- .../triggeredFunctions/triggeredFunctions.ts | 103 ++++++++-- redisinsight/ui/src/telemetry/events.ts | 11 +- .../ui/src/utils/triggered-functions/index.ts | 1 + .../ui/src/utils/triggered-functions/utils.ts | 10 + 30 files changed, 1382 insertions(+), 198 deletions(-) create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/TriggeredFunctionsTabs.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/constants.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/styles.module.scss create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/styles.module.scss create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/styles.module.scss create mode 100644 redisinsight/ui/src/utils/triggered-functions/index.ts create mode 100644 redisinsight/ui/src/utils/triggered-functions/utils.ts diff --git a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts index 4efbaf0073..2e455f028d 100644 --- a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts @@ -10,7 +10,7 @@ import ClusterDetailsPage from 'uiSrc/pages/clusterDetails' import AnalyticsPage from 'uiSrc/pages/analytics' import DatabaseAnalysisPage from 'uiSrc/pages/databaseAnalysis' import TriggeredFunctionsPage from 'uiSrc/pages/triggeredFunctions' -import { LibrariesPage } from 'uiSrc/pages/triggeredFunctions/pages' +import { LibrariesPage, FunctionsPage } from 'uiSrc/pages/triggeredFunctions/pages' import COMMON_ROUTES from './commonRoutes' const ANALYTICS_ROUTES: IRoute[] = [ @@ -39,7 +39,7 @@ const TRIGGERED_FUNCTIONS_ROUTES: IRoute[] = [ pageName: PageNames.triggeredFunctionsFunctions, path: Pages.triggeredFunctionsFunctions(':instanceId'), protected: true, - component: LibrariesPage, + component: FunctionsPage, }, { pageName: PageNames.triggeredFunctionsLibraries, diff --git a/redisinsight/ui/src/components/main-router/constants/sub-routes/triggeredFunctionsRoutes.ts b/redisinsight/ui/src/components/main-router/constants/sub-routes/triggeredFunctionsRoutes.ts index f164976780..5921c11be2 100644 --- a/redisinsight/ui/src/components/main-router/constants/sub-routes/triggeredFunctionsRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/sub-routes/triggeredFunctionsRoutes.ts @@ -1,11 +1,11 @@ import { IRoute, PageNames, Pages } from 'uiSrc/constants' -import { LibrariesPage } from 'uiSrc/pages/triggeredFunctions/pages' +import { LibrariesPage, FunctionsPage } from 'uiSrc/pages/triggeredFunctions/pages' export const TRIGGERED_FUNCTIONS_ROUTES: IRoute[] = [ { pageName: PageNames.triggeredFunctionsFunctions, path: Pages.triggeredFunctionsFunctions(':instanceId'), - component: LibrariesPage, + component: FunctionsPage, }, { pageName: PageNames.triggeredFunctionsLibraries, diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index a3b249a35c..5af4366878 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -105,6 +105,7 @@ enum ApiEndpoints { RECOMMENDATIONS_READ = 'recommendations/read', TRIGGERED_FUNCTIONS_LIBRARIES = 'triggered-functions/libraries', + TRIGGERED_FUNCTIONS_FUNCTIONS = 'triggered-functions/functions', TRIGGERED_FUNCTIONS_GET_LIBRARY = 'triggered-functions/get-library', TRIGGERED_FUNCTIONS_REPLACE_LIBRARY = 'triggered-functions/library/replace', diff --git a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx index 6d293b9238..217b39e6a8 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx @@ -1,15 +1,18 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useHistory } from 'react-router' -import { useParams } from 'react-router-dom' +import { useLocation, useParams } from 'react-router-dom' import { useSelector } from 'react-redux' import { Pages } from 'uiSrc/constants' import InstanceHeader from 'uiSrc/components/instance-header' -import AnalyticsPageRouter from 'uiSrc/pages/analytics/AnalyticsPageRouter' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' import { appAnalyticsInfoSelector } from 'uiSrc/slices/app/info' + +import TriggeredFunctionsPageRouter from './TriggeredFunctionsPageRouter' +import TriggeredFunctionsTabs from './components/TriggeredFunctionsTabs' + import styles from './styles.modules.scss' export interface Props { @@ -21,16 +24,27 @@ const TriggeredFunctionsPage = ({ routes = [] }: Props) => { const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) const [isPageViewSent, setIsPageViewSent] = useState(false) + const pathnameRef = useRef('') + const { instanceId } = useParams<{ instanceId: string }>() const history = useHistory() + const { pathname } = useLocation() const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}` setTitle(`${dbName} - Triggers & Functions`) useEffect(() => { - // TODO update routing - history.push(Pages.triggeredFunctionsLibraries(instanceId)) - }, []) + if (pathname === Pages.triggeredFunctions(instanceId)) { + if (pathnameRef.current === Pages.triggeredFunctionsLibraries(instanceId)) { + history.push(pathnameRef.current) + return + } + + history.push(Pages.triggeredFunctionsFunctions(instanceId)) + } + + pathnameRef.current = pathname === Pages.triggeredFunctions(instanceId) ? '' : pathname + }, [pathname]) useEffect(() => { if (connectedInstanceName && !isPageViewSent && analyticsIdentified) { @@ -46,11 +60,14 @@ const TriggeredFunctionsPage = ({ routes = [] }: Props) => { setIsPageViewSent(true) } + const path = pathname?.split('/').pop() || '' + return ( <>
- + +
) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/TriggeredFunctionsTabs.tsx b/redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/TriggeredFunctionsTabs.tsx new file mode 100644 index 0000000000..4afbdb5b8b --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/TriggeredFunctionsTabs.tsx @@ -0,0 +1,57 @@ +import React, { useCallback } from 'react' +import { EuiTab, EuiTabs } from '@elastic/eui' +import { useHistory, useParams } from 'react-router-dom' + +import { Pages } from 'uiSrc/constants' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { triggeredFunctionsViewTabs, TriggeredFunctionsViewTabs } from 'uiSrc/pages/triggeredFunctions/constants' + +export interface Props { + path: string +} + +const TriggeredFunctionsTabs = ({ path }: Props) => { + const history = useHistory() + + const { instanceId } = useParams<{ instanceId: string }>() + + const onSelectedTabChanged = (id: TriggeredFunctionsViewTabs) => { + if (id === TriggeredFunctionsViewTabs.Libraries) { + history.push(Pages.triggeredFunctionsLibraries(instanceId)) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARIES_CLICKED, + eventData: { + databaseId: instanceId + } + }) + } + if (id === TriggeredFunctionsViewTabs.Functions) { + history.push(Pages.triggeredFunctionsFunctions(instanceId)) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTIONS_CLICKED, + eventData: { + databaseId: instanceId + } + }) + } + } + + const renderTabs = useCallback(() => triggeredFunctionsViewTabs.map(({ id, label }) => ( + onSelectedTabChanged(id)} + key={id} + data-testid={`triggered-functions-tab-${id}`} + > + {label} + + )), [path]) + + return ( + <> + {renderTabs()} + + ) +} + +export default TriggeredFunctionsTabs diff --git a/redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/index.ts new file mode 100644 index 0000000000..8ca9098bf2 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/index.ts @@ -0,0 +1,3 @@ +import TriggeredFunctionsTabs from './TriggeredFunctionsTabs' + +export default TriggeredFunctionsTabs diff --git a/redisinsight/ui/src/pages/triggeredFunctions/constants.ts b/redisinsight/ui/src/pages/triggeredFunctions/constants.ts new file mode 100644 index 0000000000..748947e2fc --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/constants.ts @@ -0,0 +1,49 @@ +import { FunctionType } from 'uiSrc/slices/interfaces/triggeredFunctions' + +enum TriggeredFunctionsViewTabs { + Libraries = 'libraries', + Functions = 'functions' +} + +const triggeredFunctionsViewTabs: Array<{ id: TriggeredFunctionsViewTabs, label: string }> = [ + { + id: TriggeredFunctionsViewTabs.Libraries, + label: 'Libraries', + }, + { + id: TriggeredFunctionsViewTabs.Functions, + label: 'Functions', + }, +] +const LIST_OF_FUNCTION_NAMES = { + [FunctionType.Function]: 'Functions', + [FunctionType.KeyspaceTrigger]: 'Keyspace triggers', + [FunctionType.ClusterFunction]: 'Cluster Functions', + [FunctionType.StreamTrigger]: 'Stream Functions', +} + +const LIST_OF_FUNCTION_TYPES = [ + { title: 'Functions', type: FunctionType.Function }, + { title: 'Keyspace triggers', type: FunctionType.KeyspaceTrigger }, + { title: 'Cluster Functions', type: FunctionType.ClusterFunction }, + { title: 'Stream Functions', type: FunctionType.StreamTrigger }, +] + +enum LibDetailsSelectedView { + Code = 'code', + Config = 'config' +} + +const LIB_DETAILS_TABS = [ + { id: LibDetailsSelectedView.Code, label: 'Library Code' }, + { id: LibDetailsSelectedView.Config, label: 'Configuration' } +] + +export { + TriggeredFunctionsViewTabs, + triggeredFunctionsViewTabs, + LIST_OF_FUNCTION_NAMES, + LIST_OF_FUNCTION_TYPES, + LibDetailsSelectedView, + LIB_DETAILS_TABS, +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx new file mode 100644 index 0000000000..e2bbeb0ef3 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx @@ -0,0 +1,187 @@ +import React, { useEffect, useState } from 'react' +import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiResizableContainer, } from '@elastic/eui' + +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import cx from 'classnames' +import { find } from 'lodash' +import { + fetchTriggeredFunctionsFunctionsList, + setSelectedFunctionToShow, + triggeredFunctionsFunctionsSelector, +} from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { TriggeredFunctionsFunction } from 'uiSrc/slices/interfaces/triggeredFunctions' +import { Nullable } from 'uiSrc/utils' + +import { LIST_OF_FUNCTION_NAMES } from 'uiSrc/pages/triggeredFunctions/constants' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { getFunctionsLengthByType } from 'uiSrc/utils/triggered-functions/utils' +import FunctionsList from './components/FunctionsList' +import FunctionDetails from './components/FunctionDetails' + +import styles from './styles.module.scss' + +export const firstPanelId = 'libraries-left-panel' +export const secondPanelId = 'libraries-right-panel' + +const FunctionsPage = () => { + const { lastRefresh, loading, data: functions, selected } = useSelector(triggeredFunctionsFunctionsSelector) + const [items, setItems] = useState([]) + const [filterValue, setFilterValue] = useState('') + const [selectedRow, setSelectedRow] = useState>(null) + + const { instanceId } = useParams<{ instanceId: string }>() + const dispatch = useDispatch() + + useEffect(() => { + updateList() + }, []) + + useEffect(() => { + applyFiltering() + }, [filterValue, functions]) + + const updateList = () => { + dispatch(fetchTriggeredFunctionsFunctionsList(instanceId, (functionsList) => { + if (selected) { + const findRow = find(functionsList, selected) + + if (findRow) { + setSelectedRow(findRow) + } + + dispatch(setSelectedFunctionToShow(null)) + } + + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTIONS_RECEIVED, + eventData: { + databaseId: instanceId, + functions: { + total: functionsList.length, + ...getFunctionsLengthByType(functionsList) + } + } + }) + })) + } + + const onChangeFiltering = (e: React.ChangeEvent) => { + setFilterValue(e.target.value.toLowerCase()) + } + + const handleSelectRow = (item?: TriggeredFunctionsFunction) => { + setSelectedRow(item ?? null) + } + + const applyFiltering = () => { + if (!filterValue) { + setItems(functions || []) + return + } + + const itemsTemp = functions?.filter((item: TriggeredFunctionsFunction) => ( + item.name?.toLowerCase().indexOf(filterValue) !== -1 + || item.library?.toLowerCase().indexOf(filterValue) !== -1 + || (item.type && LIST_OF_FUNCTION_NAMES[item.type].toLowerCase().indexOf(filterValue) !== -1) + )) + + setItems(itemsTemp || []) + } + + return ( + + + + + {!!functions?.length && ( + + )} + + + + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + +
+ {!functions && loading && ( +
+ +
+ )} + {functions && ( + + )} +
+
+ + +
+ {selectedRow && ( + setSelectedRow(null)} /> + )} +
+
+ + )} +
+
+
+ ) +} + +export default FunctionsPage diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx new file mode 100644 index 0000000000..f93c3345ab --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx @@ -0,0 +1,142 @@ +import React from 'react' +import { cloneDeep } from 'lodash' +import { cleanup, mockedStore, render, screen, fireEvent, act } from 'uiSrc/utils/test-utils' + +import { + getTriggeredFunctionsLibraryDetails, + triggeredFunctionsSelectedLibrarySelector +} from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' + +import { + TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA +} from 'uiSrc/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler' +import FunctionDetails from './FunctionDetails' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +jest.mock('uiSrc/slices/triggeredFunctions/triggeredFunctions', () => ({ + ...jest.requireActual('uiSrc/slices/triggeredFunctions/triggeredFunctions'), + triggeredFunctionsSelectedLibrarySelector: jest.fn().mockReturnValue({ + ...jest.requireActual('uiSrc/slices/triggeredFunctions/triggeredFunctions').triggeredFunctionsSelectedLibrarySelector + }) +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('LibraryDetails', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call fetch details on render', () => { + render() + + const expectedActions = [getTriggeredFunctionsLibraryDetails()] + expect(store.getActions()).toEqual(expectedActions) + }) + + it('should call onCLose', () => { + const onClose = jest.fn() + render() + + fireEvent.click(screen.getByTestId('close-right-panel-btn')) + expect(onClose).toBeCalled() + }) + + it('should render proper content', async () => { + (triggeredFunctionsSelectedLibrarySelector as jest.Mock).mockReturnValueOnce({ + lastRefresh: null, + loading: false, + data: { + apiVersion: '1.2', + code: 'code', + configuration: 'config', + functions: [ + { name: 'foo', type: 'functions' }, + { name: 'foo1', type: 'functions' }, + { name: 'foo2', type: 'cluster_functions' }, + { name: 'foo3', type: 'keyspace_triggers' }, + ], + name: 'lib', + pendingJobs: 12, + user: 'default', + } + }) + + render() + + expect(screen.getByTestId('lib-name')).toHaveTextContent('lib') + expect(screen.getByTestId('lib-apiVersion')).toHaveTextContent('1.2') + expect(screen.getByTestId('functions-Functions')).toHaveTextContent('Functions (2)foofoo1') + expect(screen.getByTestId('functions-Keyspace triggers')).toHaveTextContent('Keyspace triggers (1)foo3') + expect(screen.getByTestId('functions-Cluster Functions')).toHaveTextContent('Cluster Functions (1)foo2') + expect(screen.getByTestId('functions-Stream Functions')).toHaveTextContent('Stream Functions Empty') + + expect(screen.getByTestId('library-code')).toHaveValue('code') + + fireEvent.click(screen.getByTestId('library-view-tab-config')) + expect(screen.getByTestId('library-configuration')).toHaveValue('config') + }) + + it('should call proper telemetry events', async () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + await act(() => { + render() + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_VIEWED, + eventData: { + databaseId: 'instanceId', + apiVersion: TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA.apiVersion, + pendingJobs: TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA.pendingJobs + } + }) + + fireEvent.click(screen.getByTestId('refresh-lib-details-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_REFRESH_CLICKED, + eventData: { + databaseId: 'instanceId' + } + }) + + sendEventTelemetry.mockRestore() + + fireEvent.click(screen.getByTestId('auto-refresh-config-btn')) + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_ENABLED, + eventData: { + refreshRate: '5.0', + databaseId: 'instanceId' + } + }) + + sendEventTelemetry.mockRestore() + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_DISABLED, + eventData: { + refreshRate: '5.0', + databaseId: 'instanceId' + } + }) + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx new file mode 100644 index 0000000000..9a782b468b --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx @@ -0,0 +1,155 @@ +import React, { useEffect } from 'react' +import { EuiBadge, EuiButtonIcon, EuiCollapsibleNavGroup, EuiLink, EuiText, EuiTitle, EuiToolTip, } from '@elastic/eui' +import cx from 'classnames' +import { isNil } from 'lodash' +import { useHistory, useParams } from 'react-router-dom' +import { useDispatch } from 'react-redux' +import { TriggeredFunctionsFunction } from 'uiSrc/slices/interfaces/triggeredFunctions' + +import { Pages } from 'uiSrc/constants' +import { setSelectedLibraryToShow } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import styles from './styles.module.scss' + +export interface Props { + item: TriggeredFunctionsFunction + onClose: () => void +} + +const FunctionDetails = (props: Props) => { + const { item, onClose } = props + const { name, library, description, flags, lastError, totalExecutionTime, lastExecutionTime } = item + + const { instanceId } = useParams<{ instanceId: string }>() + const history = useHistory() + const dispatch = useDispatch() + + useEffect(() => { + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_VIEWED, + eventData: { + databaseId: instanceId, + functionType: item.type, + isAsync: item?.isAsync + } + }) + }, [item]) + + const goToLibrary = (e: React.MouseEvent, libName: string) => { + e.preventDefault() + dispatch(setSelectedLibraryToShow(libName)) + history.push(Pages.triggeredFunctionsLibraries(instanceId)) + } + + const generateFieldValue = (field: any, title: string, value: any) => !isNil(field) && ( +
+ {title}: + {value} +
+ ) + + return ( +
+
+ + {name} + + + onClose()} + data-testid="close-right-panel-btn" + /> + +
+
+ +
+ Library: + + goToLibrary(e, library)} + data-testid={`moveToLibrary-${library}`} + > + {library} + + +
+ {generateFieldValue(item.isAsync, 'isAsync', item.isAsync ? 'Yes' : 'No')} + {generateFieldValue(item.prefix, 'Prefix', item.prefix)} + {generateFieldValue(item.trim, 'Trim', item.trim ? 'Yes' : 'No')} + {generateFieldValue(item.window, 'Window', item.window)} + {generateFieldValue(item.total, 'Total', item.total)} + {generateFieldValue(item.success, 'Success', item.success)} + {generateFieldValue(item.fail, 'Failed', item.fail)} +
+ {description && ( + + {description} + + )} + {lastError && ( + + {lastError} + + )} + {!isNil(totalExecutionTime) && ( + + {generateFieldValue(totalExecutionTime, 'Total Execution Time', totalExecutionTime)} + {generateFieldValue(lastExecutionTime, 'Last Execution Time', lastExecutionTime)} + + )} + {flags && ( + + {flags.length === 0 && (Empty)} + {flags.map((flag) => ({flag}))} + + )} +
+
+ ) +} + +const getPropsForNavGroup = (title: string): any => ({ + key: title, + isCollapsible: true, + className: styles.accordion, + title, + initialIsOpen: true, + paddingSize: 'none', + titleElement: 'span', + 'data-testid': `function-details-${title}`, +}) + +export default React.memo(FunctionDetails) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/index.ts new file mode 100644 index 0000000000..c4d613b96b --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/index.ts @@ -0,0 +1,3 @@ +import FunctionDetails from './FunctionDetails' + +export default FunctionDetails diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/styles.module.scss new file mode 100644 index 0000000000..873d1561fd --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/styles.module.scss @@ -0,0 +1,64 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/components/table/mixins"; +@import "@elastic/eui/src/global_styling/index"; + +.main { + position: relative; + height: 100%; + + .header { + height: 58px; + padding: 16px; + border-bottom: 1px solid var(--euiColorLightShade); + + .titleTooltip { + width: auto; + max-width: calc(100% - 80px); + } + } + + .content { + position: relative; + padding: 16px 16px 40px; + + @include euiScrollBar; + overflow: auto; + height: 100%; + max-height: calc(100% - 58px); + } + + .accordion { + margin-top: 0; + margin-bottom: 20px; + } + + .accordionIcon { + margin-right: -12px; + } + + .field { + display: flex; + justify-content: flex-start; + flex-wrap: wrap; + } + + .fieldName { + margin-right: 12px; + } + + .fieldValue { + word-break: break-all; + :global(.euiLink) { + color: var(--htmlColor) !important; + } + } + + .badge { + background-color: var(--browserComponentActive) !important; + color: var(--euiColorPrimary) !important; + font-weight: 400; + line-height: 1.8; + margin-top: 2px; + margin-bottom: 2px; + } +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.spec.tsx new file mode 100644 index 0000000000..858b1e100e --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.spec.tsx @@ -0,0 +1,89 @@ +import React from 'react' + +import { mock } from 'ts-mockito' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' +import { TriggeredFunctionsLibrary } from 'uiSrc/slices/interfaces/triggeredFunctions' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import FunctionsList, { Props } from './FunctionsList' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +const mockedLibraries: TriggeredFunctionsLibrary[] = [ + { + name: 'lib1', + user: 'user1', + totalFunctions: 2, + pendingJobs: 1 + }, + { + name: 'lib2', + user: 'user1', + totalFunctions: 2, + pendingJobs: 1 + }, + { + name: 'lib3', + user: 'user2', + totalFunctions: 2, + pendingJobs: 1 + } +] + +const mockedProps = mock() + +describe('LibrariesList', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render items properly', () => { + render() + + expect(screen.getByTestId('total-libraries')).toHaveTextContent('Total: 3') + expect(screen.queryAllByTestId(/^row-/).length).toEqual(3) + }) + + it('should call proper telemetry events', () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render() + + fireEvent.click(screen.getByTestId('refresh-libraries-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_REFRESH_CLICKED, + eventData: { + databaseId: 'instanceId' + } + }) + + sendEventTelemetry.mockRestore() + + fireEvent.click(screen.getByTestId('auto-refresh-config-btn')) + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_ENABLED, + eventData: { + refreshRate: '5.0', + databaseId: 'instanceId' + } + }) + + sendEventTelemetry.mockRestore() + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED, + eventData: { + refreshRate: '5.0', + databaseId: 'instanceId' + } + }) + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx new file mode 100644 index 0000000000..e43bdbaba6 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx @@ -0,0 +1,138 @@ +import React, { useState } from 'react' +import { EuiBasicTableColumn, EuiInMemoryTable, EuiText, EuiToolTip, PropertySort } from '@elastic/eui' +import cx from 'classnames' + +import { useParams } from 'react-router-dom' +import { isEqual, pick } from 'lodash' +import { Maybe, Nullable } from 'uiSrc/utils' +import AutoRefresh from 'uiSrc/pages/browser/components/auto-refresh' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { FunctionType, TriggeredFunctionsFunction } from 'uiSrc/slices/interfaces/triggeredFunctions' +import { LIST_OF_FUNCTION_NAMES } from 'uiSrc/pages/triggeredFunctions/constants' +import styles from './styles.module.scss' + +export interface Props { + items: Nullable + loading: boolean + onRefresh: () => void + lastRefresh: Nullable + selectedRow: Nullable + onSelectRow: (item: TriggeredFunctionsFunction) => void +} + +const NoFunctionsMessage: React.ReactNode = (No Functions found) + +const FunctionsList = (props: Props) => { + const { items, loading, onRefresh, lastRefresh, selectedRow, onSelectRow } = props + const [sort, setSort] = useState>(undefined) + + const { instanceId } = useParams<{ instanceId: string }>() + + const columns: EuiBasicTableColumn[] = [ + { + name: 'Function Name', + field: 'name', + sortable: true, + truncateText: true, + width: '25%', + render: (value: string) => ( + <>{value} + ) + }, + { + name: 'Library', + field: 'library', + sortable: true, + truncateText: true, + width: '25%', + render: (value: string) => ( + <>{value} + ) + }, + { + name: 'Type', + field: 'type', + sortable: true, + width: '50%', + render: (value: string) => LIST_OF_FUNCTION_NAMES[value as FunctionType] + }, + ] + + const handleSelect = (item: TriggeredFunctionsFunction) => { + onSelectRow(item) + } + + const handleRefreshClicked = () => { + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_REFRESH_CLICKED, + eventData: { + databaseId: instanceId + } + }) + } + + const handleSorting = ({ sort }: any) => { + setSort(sort) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTIONS_SORTED, + eventData: { + ...sort, + databaseId: instanceId + } + }) + } + + const handleEnableAutoRefresh = (enableAutoRefresh: boolean, refreshRate: string) => { + sendEventTelemetry({ + event: enableAutoRefresh + ? TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_AUTO_REFRESH_ENABLED + : TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_AUTO_REFRESH_DISABLED, + eventData: { + refreshRate, + databaseId: instanceId + } + }) + } + + const isRowSelected = (row: TriggeredFunctionsFunction, selectedRow: Nullable) => { + const pickFields = ['name', 'library', 'type'] + return selectedRow && isEqual(pick(row, pickFields), pick(selectedRow, pickFields)) + } + + return ( +
+
+ Total: {items?.length || 0} + onRefresh?.()} + onRefreshClicked={handleRefreshClicked} + onEnableAutoRefresh={handleEnableAutoRefresh} + testid="refresh-libraries-btn" + /> +
+ ({ + onClick: () => handleSelect(row), + className: isRowSelected(row, selectedRow) ? 'selected' : '', + 'data-testid': `row-${row.name}`, + })} + message={NoFunctionsMessage} + onTableChange={handleSorting} + className={cx('inMemoryTableDefault', 'noBorders', 'triggeredFunctions__table')} + data-testid="functions-list-table" + /> +
+ ) +} + +export default FunctionsList diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/index.ts new file mode 100644 index 0000000000..8a7f208b78 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/index.ts @@ -0,0 +1,3 @@ +import FunctionsList from './FunctionsList' + +export default FunctionsList diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/styles.module.scss new file mode 100644 index 0000000000..a590ecd8f3 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/styles.module.scss @@ -0,0 +1,37 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/components/table/mixins"; +@import "@elastic/eui/src/global_styling/index"; + +.tableWrapper { + max-height: 100%; + + .header { + height: 42px; + background-color: var(--browserTableRowEven); + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 20px; + } +} + +.table { + @include euiScrollBar; + overflow: auto; + position: relative; + max-height: calc(100% - 42px); + + :global { + thead { + border-bottom: 1px solid var(--tableDarkestBorderColor); + } + + .euiTableHeaderCell { + background-color: var(--euiColorEmptyShade); + } + + .euiTableCellContent { + padding: 16px 18px !important; + } + } +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/index.ts new file mode 100644 index 0000000000..12ad6194f4 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/index.ts @@ -0,0 +1,3 @@ +import FunctionsPage from './FunctionsPage' + +export default FunctionsPage diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/styles.module.scss new file mode 100644 index 0000000000..42e584d476 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/styles.module.scss @@ -0,0 +1,19 @@ +.main { + .loading { + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; + + flex-direction: column; + + :global { + .euiLoadingSpinner { + width: 40px; + height: 40px; + } + } + } +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx index 52c096f041..ad62798a20 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx @@ -11,13 +11,16 @@ import { import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import cx from 'classnames' +import { find } from 'lodash' import { fetchTriggeredFunctionsLibrariesList, + setSelectedLibraryToShow, setTriggeredFunctionsSelectedLibrary, - triggeredFunctionsSelector + triggeredFunctionsLibrariesSelector, } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { TriggeredFunctionsLibrary } from 'uiSrc/slices/interfaces/triggeredFunctions' import { Nullable } from 'uiSrc/utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import NoLibrariesScreen from './components/NoLibrariesScreen' import LibrariesList from './components/LibrariesList' import LibraryDetails from './components/LibraryDetails' @@ -28,7 +31,7 @@ export const firstPanelId = 'libraries-left-panel' export const secondPanelId = 'libraries-right-panel' const LibrariesPage = () => { - const { lastRefresh, loading, libraries } = useSelector(triggeredFunctionsSelector) + const { lastRefresh, loading, data: libraries, selected } = useSelector(triggeredFunctionsLibrariesSelector) const [items, setItems] = useState([]) const [filterValue, setFilterValue] = useState('') const [selectedRow, setSelectedRow] = useState>(null) @@ -45,7 +48,25 @@ const LibrariesPage = () => { }, [filterValue, libraries]) const updateList = () => { - dispatch(fetchTriggeredFunctionsLibrariesList(instanceId)) + dispatch(fetchTriggeredFunctionsLibrariesList(instanceId, (librariesList) => { + if (selected) { + const findRow = find(librariesList, (item) => item.name === selected) + + if (findRow) { + setSelectedRow(selected) + } + + dispatch(setSelectedLibraryToShow(null)) + } + + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARIES_RECEIVED, + eventData: { + databaseId: instanceId, + total: librariesList.length + } + }) + })) } const onChangeFiltering = (e: React.ChangeEvent) => { @@ -76,20 +97,25 @@ const LibrariesPage = () => { } return ( - + {!!libraries?.length && ( { - - + {(EuiResizablePanel, EuiResizableButton) => ( <> @@ -122,13 +147,13 @@ const LibrariesPage = () => { minSize="550px" paddingSize="none" wrapperProps={{ - className: cx(styles.resizePanelLeft, { - [styles.fullWidth]: !selectedRow, - [styles.withSelectedRow]: selectedRow, + className: cx('triggeredFunctions__resizePanelLeft', { + fullWidth: !selectedRow, + openedRightPanel: selectedRow, }), }} > -
+
{!libraries && loading && (
@@ -150,8 +175,8 @@ const LibrariesPage = () => {
@@ -162,12 +187,12 @@ const LibrariesPage = () => { minSize="400px" paddingSize="none" wrapperProps={{ - className: cx(styles.resizePanelRight, { - [styles.noVisible]: !selectedRow + className: cx('triggeredFunctions__resizePanelRight', { + noVisible: !selectedRow }), }} > -
+
{selectedRow && ( { } return ( -
-
+
+
Total: {items?.length || 0} { })} message={NoLibrariesMessage} onTableChange={handleSorting} - className={cx('inMemoryTableDefault', 'noBorders', styles.table)} + className={cx('inMemoryTableDefault', 'noBorders', 'triggeredFunctions__table')} data-testid="libraries-list-table" />
diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx index e10f386985..e324b9d906 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' +import { useHistory, useParams } from 'react-router-dom' import { EuiButtonIcon, EuiFlexGroup, @@ -13,12 +13,13 @@ import { EuiLoadingContent, EuiTabs, EuiTab, - EuiProgress + EuiProgress, + EuiLink } from '@elastic/eui' import cx from 'classnames' import { fetchTriggeredFunctionsLibrary, - replaceTriggeredFunctionsLibraryAction, + replaceTriggeredFunctionsLibraryAction, setSelectedFunctionToShow, triggeredFunctionsSelectedLibrarySelector } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' @@ -29,6 +30,13 @@ import { FunctionType } from 'uiSrc/slices/interfaces/triggeredFunctions' import AutoRefresh from 'uiSrc/pages/browser/components/auto-refresh' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { + LIB_DETAILS_TABS, + LibDetailsSelectedView, + LIST_OF_FUNCTION_TYPES +} from 'uiSrc/pages/triggeredFunctions/constants' +import { Pages } from 'uiSrc/constants' +import { getFunctionsLengthByType } from 'uiSrc/utils/triggered-functions/utils' import styles from './styles.module.scss' export interface Props { @@ -36,32 +44,16 @@ export interface Props { onClose: () => void } -const LIST_OF_FUNCTION_TYPES = [ - { title: 'Functions', type: FunctionType.Function }, - { title: 'Keyspace triggers', type: FunctionType.KeyspaceTrigger }, - { title: 'Cluster Functions', type: FunctionType.ClusterFunction }, - { title: 'Stream Functions', type: FunctionType.StreamTrigger }, -] - -enum SelectedView { - Code = 'code', - Config = 'config' -} - -const tabs = [ - { id: SelectedView.Code, label: 'Library Code' }, - { id: SelectedView.Config, label: 'Configuration' } -] - const LibraryDetails = (props: Props) => { const { name, onClose } = props const { loading, lastRefresh, data: library } = useSelector(triggeredFunctionsSelectedLibrarySelector) - const [selectedView, setSelectedView] = useState(tabs[0].id) + const [selectedView, setSelectedView] = useState(LIB_DETAILS_TABS[0].id) const [configuration, setConfiguration] = useState('_') const [code, setCode] = useState('_') const { instanceId } = useParams<{ instanceId: string }>() + const history = useHistory() const dispatch = useDispatch() useEffect(() => { @@ -74,7 +66,12 @@ const LibraryDetails = (props: Props) => { eventData: { databaseId: instanceId, pendingJobs: lib?.pendingJobs || 0, - apiVersion: lib?.apiVersion || '1.0' + apiVersion: lib?.apiVersion || '1.0', + configLoaded: lib?.code || false, + functions: { + total: lib?.functions.length || 0, + ...getFunctionsLengthByType(lib?.functions) + } } }) } @@ -123,7 +120,7 @@ const LibraryDetails = (props: Props) => { dispatch(replaceTriggeredFunctionsLibraryAction(instanceId, code, configuration, () => { closeEditor() sendEventTelemetry({ - event: selectedView === SelectedView.Code + event: selectedView === LibDetailsSelectedView.Code ? TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_CODE_REPLACED : TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_CONFIGURATION_REPLACED, eventData: { @@ -138,6 +135,19 @@ const LibraryDetails = (props: Props) => { setCode(library?.code ?? '') } + const goToFunction = ( + e: React.MouseEvent, + { functionName, type }: { functionName: string, type: FunctionType } + ) => { + e.preventDefault() + dispatch(setSelectedFunctionToShow({ + name: functionName, + type, + library: name + })) + history.push(Pages.triggeredFunctionsFunctions(instanceId)) + } + const functionGroup = (title: string, list: Array<{ type: FunctionType, name: string }>, initialIsOpen = false) => { const count = list.length > 0 ? `(${list.length})` : '' return ( @@ -153,13 +163,19 @@ const LibraryDetails = (props: Props) => { > {list.length ? (
    - {list.map(({ name }) => ( + {list.map(({ name, type }) => (
  • - {name} + goToFunction(e, { functionName: name, type })} + data-testid={`moveToFunction-${name}`} + > + {name} +
  • ))}
@@ -175,7 +191,7 @@ const LibraryDetails = (props: Props) => { return functionGroup(title, functionsList, functionsList.length > 0) }) - const renderTabs = useCallback(() => tabs.map(({ id, label }) => ( + const renderTabs = useCallback(() => LIB_DETAILS_TABS.map(({ id, label }) => ( setSelectedView(id)} @@ -197,8 +213,8 @@ const LibraryDetails = (props: Props) => { > {name} - - + + {library?.apiVersion && (API: {library.apiVersion})} @@ -219,7 +235,7 @@ const LibraryDetails = (props: Props) => { { <> {renderFunctionsLists()} {renderTabs()} - {selectedView === SelectedView.Code && ( + {selectedView === LibDetailsSelectedView.Code && ( { data-testid="library-code" /> )} - {selectedView === SelectedView.Config && ( + {selectedView === LibDetailsSelectedView.Config && ( + lastError?: Nullable + lastExecutionTime?: number + totalExecutionTime?: number + prefix?: string + trim?: boolean + window?: number +} + export interface StateTriggeredFunctions { - libraries: Nullable + libraries: { + data: Nullable + loading: boolean, + lastRefresh: Nullable + error: string + selected: Nullable + } + functions: { + data: Nullable + loading: boolean, + lastRefresh: Nullable + error: string + selected: Nullable + } selectedLibrary: { lastRefresh: Nullable data: Nullable loading: boolean } - loading: boolean, - lastRefresh: Nullable - error: string } diff --git a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts index cb611b3a14..a080263321 100644 --- a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts +++ b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts @@ -1,6 +1,11 @@ -import { createSlice } from '@reduxjs/toolkit' +import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AxiosError } from 'axios' -import { StateTriggeredFunctions, TriggeredFunctionsLibraryDetails } from 'uiSrc/slices/interfaces/triggeredFunctions' +import { + StateTriggeredFunctions, + TriggeredFunctionsFunction, + TriggeredFunctionsLibrary, + TriggeredFunctionsLibraryDetails +} from 'uiSrc/slices/interfaces/triggeredFunctions' import { AppDispatch, RootState } from 'uiSrc/slices/store' import { apiService } from 'uiSrc/services' import { getApiErrorMessage, getUrl, isStatusSuccessful, Nullable } from 'uiSrc/utils' @@ -8,15 +13,25 @@ import { ApiEndpoints } from 'uiSrc/constants' import { addErrorNotification } from 'uiSrc/slices/app/notifications' export const initialState: StateTriggeredFunctions = { - libraries: null, + libraries: { + data: null, + loading: false, + lastRefresh: null, + error: '', + selected: null, + }, + functions: { + data: null, + loading: false, + lastRefresh: null, + error: '', + selected: null + }, selectedLibrary: { lastRefresh: null, loading: false, data: null }, - loading: false, - lastRefresh: null, - error: '', } const triggeredFunctionsSlice = createSlice({ @@ -25,17 +40,30 @@ const triggeredFunctionsSlice = createSlice({ reducers: { setTriggeredFunctionsInitialState: () => initialState, getTriggeredFunctionsLibrariesList: (state) => { - state.loading = true - state.error = '' + state.libraries.loading = true + state.libraries.error = '' }, getTriggeredFunctionsLibrariesListSuccess: (state, { payload }) => { - state.loading = false - state.lastRefresh = Date.now() - state.libraries = payload + state.libraries.loading = false + state.libraries.lastRefresh = Date.now() + state.libraries.data = payload }, getTriggeredFunctionsLibrariesListFailure: (state, { payload }) => { - state.loading = false - state.error = payload + state.libraries.loading = false + state.libraries.error = payload + }, + getTriggeredFunctionsFunctionsList: (state) => { + state.functions.loading = true + state.functions.error = '' + }, + getTriggeredFunctionsFunctionsListSuccess: (state, { payload }) => { + state.functions.loading = false + state.functions.lastRefresh = Date.now() + state.functions.data = payload + }, + getTriggeredFunctionsFunctionsListFailure: (state, { payload }) => { + state.functions.loading = false + state.functions.error = payload }, setTriggeredFunctionsSelectedLibrary: (state, { payload }) => { state.selectedLibrary.data = payload @@ -59,7 +87,13 @@ const triggeredFunctionsSlice = createSlice({ }, replaceTriggeredFunctionsLibraryFailure: (state) => { state.selectedLibrary.loading = false - } + }, + setSelectedFunctionToShow: (state, { payload }: PayloadAction>) => { + state.functions.selected = payload + }, + setSelectedLibraryToShow: (state, { payload }: PayloadAction>) => { + state.libraries.selected = payload + }, } }) @@ -67,6 +101,9 @@ export const { setTriggeredFunctionsInitialState, getTriggeredFunctionsLibrariesList, getTriggeredFunctionsLibrariesListSuccess, + getTriggeredFunctionsFunctionsList, + getTriggeredFunctionsFunctionsListSuccess, + getTriggeredFunctionsFunctionsListFailure, getTriggeredFunctionsLibrariesListFailure, setTriggeredFunctionsSelectedLibrary, getTriggeredFunctionsLibraryDetails, @@ -75,9 +112,13 @@ export const { replaceTriggeredFunctionsLibrary, replaceTriggeredFunctionsLibrarySuccess, replaceTriggeredFunctionsLibraryFailure, + setSelectedFunctionToShow, + setSelectedLibraryToShow, } = triggeredFunctionsSlice.actions export const triggeredFunctionsSelector = (state: RootState) => state.triggeredFunctions +export const triggeredFunctionsLibrariesSelector = (state: RootState) => state.triggeredFunctions.libraries +export const triggeredFunctionsFunctionsSelector = (state: RootState) => state.triggeredFunctions.functions export const triggeredFunctionsSelectedLibrarySelector = (state: RootState) => state.triggeredFunctions.selectedLibrary export default triggeredFunctionsSlice.reducer @@ -85,7 +126,7 @@ export default triggeredFunctionsSlice.reducer // Asynchronous thunk action export function fetchTriggeredFunctionsLibrariesList( instanceId: string, - onSuccessAction?: () => void, + onSuccessAction?: (data: TriggeredFunctionsLibrary[]) => void, onFailAction?: () => void, ) { return async (dispatch: AppDispatch) => { @@ -101,7 +142,7 @@ export function fetchTriggeredFunctionsLibrariesList( if (isStatusSuccessful(status)) { dispatch(getTriggeredFunctionsLibrariesListSuccess(data)) - onSuccessAction?.() + onSuccessAction?.(data) } } catch (_err) { const error = _err as AxiosError @@ -113,6 +154,36 @@ export function fetchTriggeredFunctionsLibrariesList( } } +export function fetchTriggeredFunctionsFunctionsList( + instanceId: string, + onSuccessAction?: (data: TriggeredFunctionsFunction[]) => void, + onFailAction?: () => void, +) { + return async (dispatch: AppDispatch) => { + try { + dispatch(getTriggeredFunctionsFunctionsList()) + + const { data, status } = await apiService.get( + getUrl( + instanceId, + ApiEndpoints.TRIGGERED_FUNCTIONS_FUNCTIONS, + ) + ) + + if (isStatusSuccessful(status)) { + dispatch(getTriggeredFunctionsFunctionsListSuccess(data)) + onSuccessAction?.(data) + } + } catch (_err) { + const error = _err as AxiosError + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + dispatch(getTriggeredFunctionsFunctionsListFailure(errorMessage)) + onFailAction?.() + } + } +} + export function fetchTriggeredFunctionsLibrary( instanceId: string, libName: string, diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index e7aee8fe7b..68ac32a7c0 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -247,5 +247,14 @@ export enum TelemetryEvent { TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_ENABLED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_ENABLED', TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_DISABLED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_DISABLED', TRIGGERS_AND_FUNCTIONS_LIBRARY_CODE_REPLACED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_CODE_REPLACED', - TRIGGERS_AND_FUNCTIONS_LIBRARY_CONFIGURATION_REPLACED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_CONFIGURATION_REPLACED' + TRIGGERS_AND_FUNCTIONS_LIBRARY_CONFIGURATION_REPLACED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_CONFIGURATION_REPLACED', + TRIGGERS_AND_FUNCTIONS_LIBRARIES_CLICKED = 'TRIGGERS_AND_FUNCTIONS_LIBRARIES_CLICKED', + TRIGGERS_AND_FUNCTIONS_FUNCTIONS_CLICKED = 'TRIGGERS_AND_FUNCTIONS_FUNCTIONS_CLICKED', + TRIGGERS_AND_FUNCTIONS_FUNCTIONS_SORTED = 'TRIGGERS_AND_FUNCTIONS_FUNCTIONS_SORTED', + TRIGGERS_AND_FUNCTIONS_FUNCTION_VIEWED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_VIEWED', + TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_REFRESH_CLICKED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_REFRESH_CLICKED', + TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_AUTO_REFRESH_ENABLED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_AUTO_REFRESH_ENABLED', + TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_AUTO_REFRESH_DISABLED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_AUTO_REFRESH_DISABLED', + TRIGGERS_AND_FUNCTIONS_LIBRARIES_RECEIVED = 'TRIGGERS_AND_FUNCTIONS_LIBRARIES_RECEIVED', + TRIGGERS_AND_FUNCTIONS_FUNCTIONS_RECEIVED = 'TRIGGERS_AND_FUNCTIONS_FUNCTIONS_RECEIVED', } diff --git a/redisinsight/ui/src/utils/triggered-functions/index.ts b/redisinsight/ui/src/utils/triggered-functions/index.ts new file mode 100644 index 0000000000..9c56149efa --- /dev/null +++ b/redisinsight/ui/src/utils/triggered-functions/index.ts @@ -0,0 +1 @@ +export * from './utils' diff --git a/redisinsight/ui/src/utils/triggered-functions/utils.ts b/redisinsight/ui/src/utils/triggered-functions/utils.ts new file mode 100644 index 0000000000..1f4b732b1f --- /dev/null +++ b/redisinsight/ui/src/utils/triggered-functions/utils.ts @@ -0,0 +1,10 @@ +import { LIST_OF_FUNCTION_TYPES } from 'uiSrc/pages/triggeredFunctions/constants' +import { FunctionType } from 'uiSrc/slices/interfaces/triggeredFunctions' + +export const getFunctionsLengthByType = (functions: Array<{ + type: FunctionType + name: string +}> = []) => LIST_OF_FUNCTION_TYPES.reduce((current, next) => ({ + ...current, + [next.type]: functions?.filter((f) => f.type === next.type).length || 0 +}), {}) From 4b596aece018aaeb4fdfd577d96a8df3de867405 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 27 Jun 2023 15:22:58 +0200 Subject: [PATCH 021/106] #RI-4581 - add unit tests --- .../ui/src/mocks/data/triggeredFunctions.ts | 85 +++++++ .../triggeredFunctionsHandler.ts | 38 ++-- .../TriggeredFunctionsTabs.spec.tsx | 62 +++++ .../pages/Functions/FunctionsPage.spec.tsx | 136 +++++++++++ .../pages/Functions/FunctionsPage.tsx | 12 +- .../FunctionDetails/FunctionDetails.spec.tsx | 159 +++++++------ .../FunctionsList/FunctionsList.spec.tsx | 43 +--- .../FunctionsList/FunctionsList.tsx | 6 +- .../pages/Libraries/LibrariesPage.spec.tsx | 74 +++--- .../LibraryDetails/LibraryDetails.spec.tsx | 57 +++-- .../LibraryDetails/LibraryDetails.tsx | 3 +- .../triggeredFunctions.spec.ts | 213 ++++++++++++++++-- .../tests/triggered-functions/utils.spec.ts | 28 +++ 13 files changed, 716 insertions(+), 200 deletions(-) create mode 100644 redisinsight/ui/src/mocks/data/triggeredFunctions.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/TriggeredFunctionsTabs.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx create mode 100644 redisinsight/ui/src/utils/tests/triggered-functions/utils.spec.ts diff --git a/redisinsight/ui/src/mocks/data/triggeredFunctions.ts b/redisinsight/ui/src/mocks/data/triggeredFunctions.ts new file mode 100644 index 0000000000..cc544b64be --- /dev/null +++ b/redisinsight/ui/src/mocks/data/triggeredFunctions.ts @@ -0,0 +1,85 @@ +import { + TriggeredFunctionsFunction, + FunctionType, + TriggeredFunctionsLibrary +} from 'uiSrc/slices/interfaces/triggeredFunctions' + +export const TRIGGERED_FUNCTIONS_LIBRARIES_LIST_MOCKED_DATA: TriggeredFunctionsLibrary[] = [ + { + name: 'lib1', + user: 'user1', + totalFunctions: 2, + pendingJobs: 1 + }, + { + name: 'lib2', + user: 'user1', + totalFunctions: 2, + pendingJobs: 1 + }, + { + name: 'lib3', + user: 'user2', + totalFunctions: 2, + pendingJobs: 1 + } +] + +export const TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA = { + apiVersion: '1.2', + code: 'code', + configuration: 'config', + functions: [ + { name: 'foo', type: 'functions' }, + { name: 'foo1', type: 'functions' }, + { name: 'foo2', type: 'cluster_functions' }, + { name: 'foo3', type: 'keyspace_triggers' }, + ], + name: 'lib', + pendingJobs: 12, + user: 'default', +} + +export const TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA: TriggeredFunctionsFunction[] = [ + { + name: 'foo1', + isAsync: false, + flags: ['flag', 'flag2'], + description: 'descriptions my description', + type: 'functions' as FunctionType, + library: 'libStringLong' + }, + { + name: 'foo2', + isAsync: true, + flags: ['flag', 'flag2'], + type: 'functions' as FunctionType, + library: 'libStringLong' + }, + { + name: 'foo3', + type: 'cluster_functions' as FunctionType, + library: 'libStringLong' + }, + { + name: 'foo4', + success: 3, + fail: 1, + total: 4, + lastError: 'Some error', + lastExecutionTime: 39, + totalExecutionTime: 39, + description: 'description', + type: 'keyspace_triggers' as FunctionType, + library: 'libStringLong' + }, + { + name: 'foo5', + prefix: 'name', + trim: false, + window: 1, + description: null, + type: 'stream_triggers' as FunctionType, + library: 'libStringLong' + }, +] diff --git a/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts b/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts index dfbff35cb9..40febff0d5 100644 --- a/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts +++ b/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts @@ -3,31 +3,37 @@ import { getMswURL } from 'uiSrc/utils/test-utils' import { getUrl } from 'uiSrc/utils' import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers' import { ApiEndpoints } from 'uiSrc/constants' - -export const TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA = { - apiVersion: '1.2', - code: 'code', - configuration: 'config', - functions: [ - { name: 'foo', type: 'functions' }, - { name: 'foo1', type: 'functions' }, - { name: 'foo2', type: 'cluster_functions' }, - { name: 'foo3', type: 'keyspace_triggers' }, - ], - name: 'lib', - pendingJobs: 12, - user: 'default', -} +import { + TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA, + TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA, + TRIGGERED_FUNCTIONS_LIBRARIES_LIST_MOCKED_DATA, +} from 'uiSrc/mocks/data/triggeredFunctions' const handlers: RestHandler[] = [ + // fetch libraries list + rest.get( + getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.TRIGGERED_FUNCTIONS_LIBRARIES)), + async (_req, res, ctx) => res( + ctx.status(200), + ctx.json(TRIGGERED_FUNCTIONS_LIBRARIES_LIST_MOCKED_DATA), + ) + ), // fetch triggered functions lib details rest.post(getMswURL( getUrl(INSTANCE_ID_MOCK, ApiEndpoints.TRIGGERED_FUNCTIONS_GET_LIBRARY) ), - async (req, res, ctx) => res( + async (_req, res, ctx) => res( ctx.status(200), ctx.json(TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA), )), + // fetch functions list + rest.get( + getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.TRIGGERED_FUNCTIONS_FUNCTIONS)), + async (_req, res, ctx) => res( + ctx.status(200), + ctx.json(TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA), + ) + ) ] export default handlers diff --git a/redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/TriggeredFunctionsTabs.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/TriggeredFunctionsTabs.spec.tsx new file mode 100644 index 0000000000..846f172cfe --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/components/TriggeredFunctionsTabs/TriggeredFunctionsTabs.spec.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import reactRouterDom from 'react-router-dom' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import TriggeredFunctionsTabs from './TriggeredFunctionsTabs' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: jest.fn, + }), +})) + +describe('TriggeredFunctionsTabs', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call proper history push after click on tabs', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + fireEvent.click(screen.getByTestId('triggered-functions-tab-libraries')) + expect(pushMock).toBeCalledWith('/instanceId/triggered-functions/libraries') + + fireEvent.click(screen.getByTestId('triggered-functions-tab-functions')) + expect(pushMock).toBeCalledWith('/instanceId/triggered-functions/functions') + }) + + it('should call proper telemetry events', () => { + const sendEventTelemetryMock = jest.fn() + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render() + + fireEvent.click(screen.getByTestId('triggered-functions-tab-libraries')) + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARIES_CLICKED, + eventData: { + databaseId: 'instanceId', + } + }) + sendEventTelemetry.mockRestore() + + fireEvent.click(screen.getByTestId('triggered-functions-tab-functions')) + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTIONS_CLICKED, + eventData: { + databaseId: 'instanceId', + } + }) + sendEventTelemetry.mockRestore() + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx new file mode 100644 index 0000000000..85de5307db --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx @@ -0,0 +1,136 @@ +import React from 'react' +import { cloneDeep } from 'lodash' +import { cleanup, mockedStore, render, screen, fireEvent, act } from 'uiSrc/utils/test-utils' + +import { + getTriggeredFunctionsFunctionsList, + triggeredFunctionsFunctionsSelector, +} from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA } from 'uiSrc/mocks/data/triggeredFunctions' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import FunctionsPage from './FunctionsPage' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +jest.mock('uiSrc/slices/triggeredFunctions/triggeredFunctions', () => ({ + ...jest.requireActual('uiSrc/slices/triggeredFunctions/triggeredFunctions'), + triggeredFunctionsFunctionsSelector: jest.fn().mockReturnValue({ + ...jest.requireActual('uiSrc/slices/triggeredFunctions/triggeredFunctions').triggeredFunctionsFunctionsSelector + }), +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +const mockedFunctions = TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA + +describe('FunctionsPage', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should fetch list of functions', () => { + (triggeredFunctionsFunctionsSelector as jest.Mock).mockReturnValueOnce({ + data: null, + loading: false + }) + render() + + const expectedActions = [getTriggeredFunctionsFunctionsList()] + expect(store.getActions()).toEqual(expectedActions) + }) + + it('should render libraries list', async () => { + (triggeredFunctionsFunctionsSelector as jest.Mock).mockReturnValueOnce({ + data: mockedFunctions, + loading: false + }) + render() + + expect(screen.getByTestId('functions-list-table')).toBeInTheDocument() + expect(screen.queryAllByTestId(/^row-/).length).toEqual(5) + }) + + it('should filter functions list', () => { + (triggeredFunctionsFunctionsSelector as jest.Mock).mockReturnValueOnce({ + data: mockedFunctions, + loading: false + }) + render() + + fireEvent.change( + screen.getByTestId('search-functions-list'), + { target: { value: 'foo1' } } + ) + expect(screen.queryAllByTestId(/^row-/).length).toEqual(1) + expect(screen.getByTestId('row-foo1')).toBeInTheDocument() + + fireEvent.change( + screen.getByTestId('search-functions-list'), + { target: { value: 'Functions' } } + ) + + expect(screen.queryAllByTestId(/^row-/).length).toEqual(4) + expect(screen.getByTestId('row-foo1')).toBeInTheDocument() + expect(screen.getByTestId('row-foo2')).toBeInTheDocument() + expect(screen.getByTestId('row-foo3')).toBeInTheDocument() + expect(screen.getByTestId('row-foo5')).toBeInTheDocument() + + fireEvent.change( + screen.getByTestId('search-functions-list'), + { target: { value: 'libStringLong' } } + ) + + expect(screen.queryAllByTestId(/^row-/).length).toEqual(5) + + fireEvent.change( + screen.getByTestId('search-functions-list'), + { target: { value: '' } } + ) + expect(screen.queryAllByTestId(/^row-/).length).toEqual(5) + }) + + it('should open function details', () => { + (triggeredFunctionsFunctionsSelector as jest.Mock).mockReturnValueOnce({ + data: mockedFunctions, + loading: false + }) + render() + + fireEvent.click(screen.getByTestId('row-foo1')) + + expect(screen.getByTestId('function-details-foo1')).toBeInTheDocument() + }) + + it('should call proper telemetry events', async () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + await act(() => { + render() + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTIONS_RECEIVED, + eventData: { + databaseId: 'instanceId', + functions: { + cluster_functions: 1, + functions: 2, + keyspace_triggers: 1, + stream_triggers: 1, + total: 5 + } + } + }) + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx index e2bbeb0ef3..bb251e83f3 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx @@ -21,8 +21,8 @@ import FunctionDetails from './components/FunctionDetails' import styles from './styles.module.scss' -export const firstPanelId = 'libraries-left-panel' -export const secondPanelId = 'libraries-right-panel' +export const firstPanelId = 'functions-left-panel' +export const secondPanelId = 'functions-right-panel' const FunctionsPage = () => { const { lastRefresh, loading, data: functions, selected } = useSelector(triggeredFunctionsFunctionsSelector) @@ -110,8 +110,8 @@ const FunctionsPage = () => { placeholder="Search for Functions" className="triggeredFunctions__search" onChange={onChangeFiltering} - aria-label="Search libraries" - data-testid="search-libraries-list" + aria-label="Search functions" + data-testid="search-functions-list" /> )} @@ -136,7 +136,7 @@ const FunctionsPage = () => { >
{!functions && loading && ( -
+
)} @@ -156,7 +156,7 @@ const FunctionsPage = () => { className={cx('triggeredFunctions__resizableButton', { hidden: !selectedRow, })} - data-test-subj="resize-btn-libraries" + data-test-subj="resize-btn-functions" /> ({ @@ -19,11 +18,11 @@ jest.mock('uiSrc/telemetry', () => ({ sendEventTelemetry: jest.fn(), })) -jest.mock('uiSrc/slices/triggeredFunctions/triggeredFunctions', () => ({ - ...jest.requireActual('uiSrc/slices/triggeredFunctions/triggeredFunctions'), - triggeredFunctionsSelectedLibrarySelector: jest.fn().mockReturnValue({ - ...jest.requireActual('uiSrc/slices/triggeredFunctions/triggeredFunctions').triggeredFunctionsSelectedLibrarySelector - }) +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: jest.fn, + }), })) let store: typeof mockedStore @@ -33,59 +32,56 @@ beforeEach(() => { store.clearActions() }) -describe('LibraryDetails', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) +const mockedItem = TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA[0] +const mockedItemKeySpaceTriggers = TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA[3] - it('should call fetch details on render', () => { - render() - - const expectedActions = [getTriggeredFunctionsLibraryDetails()] - expect(store.getActions()).toEqual(expectedActions) +describe('FunctionDetails', () => { + it('should render', () => { + expect(render()).toBeTruthy() }) - it('should call onCLose', () => { + it('should call onClose', () => { const onClose = jest.fn() - render() + render() fireEvent.click(screen.getByTestId('close-right-panel-btn')) expect(onClose).toBeCalled() }) - it('should render proper content', async () => { - (triggeredFunctionsSelectedLibrarySelector as jest.Mock).mockReturnValueOnce({ - lastRefresh: null, - loading: false, - data: { - apiVersion: '1.2', - code: 'code', - configuration: 'config', - functions: [ - { name: 'foo', type: 'functions' }, - { name: 'foo1', type: 'functions' }, - { name: 'foo2', type: 'cluster_functions' }, - { name: 'foo3', type: 'keyspace_triggers' }, - ], - name: 'lib', - pendingJobs: 12, - user: 'default', - } - }) + it('should render proper content for functions type', async () => { + render() - render() + expect(screen.getByTestId('function-name')).toHaveTextContent(mockedItem.name) + expect(screen.getByTestId('function-details-General')).toHaveTextContent([ + 'General', + 'Library:', + mockedItem.library, + 'isAsync:', + mockedItem.isAsync ? 'Yes' : 'No' + ].join('')) - expect(screen.getByTestId('lib-name')).toHaveTextContent('lib') - expect(screen.getByTestId('lib-apiVersion')).toHaveTextContent('1.2') - expect(screen.getByTestId('functions-Functions')).toHaveTextContent('Functions (2)foofoo1') - expect(screen.getByTestId('functions-Keyspace triggers')).toHaveTextContent('Keyspace triggers (1)foo3') - expect(screen.getByTestId('functions-Cluster Functions')).toHaveTextContent('Cluster Functions (1)foo2') - expect(screen.getByTestId('functions-Stream Functions')).toHaveTextContent('Stream Functions Empty') - - expect(screen.getByTestId('library-code')).toHaveValue('code') + expect(screen.getByTestId('function-details-Description')).toHaveTextContent(mockedItem.description!) + expect(screen.getByTestId('function-details-Flags')).toHaveTextContent(mockedItem.flags?.join('')!) + }) - fireEvent.click(screen.getByTestId('library-view-tab-config')) - expect(screen.getByTestId('library-configuration')).toHaveValue('config') + it('should render proper content for keyspace triggers type', async () => { + render() + + expect(screen.getByTestId('function-name')).toHaveTextContent(mockedItemKeySpaceTriggers.name) + expect(screen.getByTestId('function-details-General')).toHaveTextContent([ + 'General', + 'Library:', + mockedItemKeySpaceTriggers.library, + 'Total:', + mockedItemKeySpaceTriggers.total, + 'Success:', + mockedItemKeySpaceTriggers.success, + 'Failed:', + mockedItemKeySpaceTriggers.fail, + ].join('')) + + expect(screen.getByTestId('function-details-Description')).toHaveTextContent(mockedItemKeySpaceTriggers.description!) + expect(screen.queryByTestId('function-details-Flags')).not.toBeInTheDocument() }) it('should call proper telemetry events', async () => { @@ -93,50 +89,51 @@ describe('LibraryDetails', () => { sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) - await act(() => { - render() - }) + render() expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_VIEWED, + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_VIEWED, eventData: { databaseId: 'instanceId', - apiVersion: TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA.apiVersion, - pendingJobs: TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA.pendingJobs + functionType: mockedItem.type, + isAsync: mockedItem.isAsync } }) - fireEvent.click(screen.getByTestId('refresh-lib-details-btn')) + sendEventTelemetry.mockRestore() + }) - expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_REFRESH_CLICKED, - eventData: { - databaseId: 'instanceId' - } - }) + it('should call proper telemetry events', async () => { + const sendEventTelemetryMock = jest.fn() - sendEventTelemetry.mockRestore() + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) - fireEvent.click(screen.getByTestId('auto-refresh-config-btn')) - fireEvent.click(screen.getByTestId('auto-refresh-switch')) + render() expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_ENABLED, + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_VIEWED, eventData: { - refreshRate: '5.0', - databaseId: 'instanceId' + databaseId: 'instanceId', + functionType: mockedItemKeySpaceTriggers.type, + isAsync: mockedItemKeySpaceTriggers.isAsync } }) sendEventTelemetry.mockRestore() - fireEvent.click(screen.getByTestId('auto-refresh-switch')) + }) - expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_DISABLED, - eventData: { - refreshRate: '5.0', - databaseId: 'instanceId' - } - }) + it('should call proper actions and history to move to the library', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + fireEvent.click(screen.getByTestId('moveToLibrary-libStringLong')) + + const expectedActions = [setSelectedLibraryToShow('libStringLong')] + expect(store.getActions()).toEqual(expectedActions) + + expect(pushMock).toHaveBeenCalledTimes(1) + expect(pushMock).toHaveBeenCalledWith('/instanceId/triggered-functions/libraries') }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.spec.tsx index 858b1e100e..faed4cd6b4 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.spec.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.spec.tsx @@ -2,8 +2,8 @@ import React from 'react' import { mock } from 'ts-mockito' import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' -import { TriggeredFunctionsLibrary } from 'uiSrc/slices/interfaces/triggeredFunctions' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA } from 'uiSrc/mocks/data/triggeredFunctions' import FunctionsList, { Props } from './FunctionsList' jest.mock('uiSrc/telemetry', () => ({ @@ -11,39 +11,20 @@ jest.mock('uiSrc/telemetry', () => ({ sendEventTelemetry: jest.fn(), })) -const mockedLibraries: TriggeredFunctionsLibrary[] = [ - { - name: 'lib1', - user: 'user1', - totalFunctions: 2, - pendingJobs: 1 - }, - { - name: 'lib2', - user: 'user1', - totalFunctions: 2, - pendingJobs: 1 - }, - { - name: 'lib3', - user: 'user2', - totalFunctions: 2, - pendingJobs: 1 - } -] +const mockedFunctions = TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA const mockedProps = mock() -describe('LibrariesList', () => { +describe('FunctionsList', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) it('should render items properly', () => { - render() + render() - expect(screen.getByTestId('total-libraries')).toHaveTextContent('Total: 3') - expect(screen.queryAllByTestId(/^row-/).length).toEqual(3) + expect(screen.getByTestId('total-functions')).toHaveTextContent('Total: 5') + expect(screen.queryAllByTestId(/^row-/).length).toEqual(5) }) it('should call proper telemetry events', () => { @@ -51,12 +32,12 @@ describe('LibrariesList', () => { sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) - render() + render() - fireEvent.click(screen.getByTestId('refresh-libraries-btn')) + fireEvent.click(screen.getByTestId('refresh-functions-btn')) expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_REFRESH_CLICKED, + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_REFRESH_CLICKED, eventData: { databaseId: 'instanceId' } @@ -68,7 +49,7 @@ describe('LibrariesList', () => { fireEvent.click(screen.getByTestId('auto-refresh-switch')) expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_ENABLED, + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_AUTO_REFRESH_ENABLED, eventData: { refreshRate: '5.0', databaseId: 'instanceId' @@ -79,7 +60,7 @@ describe('LibrariesList', () => { fireEvent.click(screen.getByTestId('auto-refresh-switch')) expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LIST_AUTO_REFRESH_DISABLED, + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_AUTO_REFRESH_DISABLED, eventData: { refreshRate: '5.0', databaseId: 'instanceId' diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx index e43bdbaba6..690386f0c0 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx @@ -102,17 +102,17 @@ const FunctionsList = (props: Props) => { return (
- Total: {items?.length || 0} + Total: {items?.length || 0} onRefresh?.()} onRefreshClicked={handleRefreshClicked} onEnableAutoRefresh={handleEnableAutoRefresh} - testid="refresh-libraries-btn" + testid="refresh-functions-btn" />
({ ...jest.requireActual('uiSrc/slices/triggeredFunctions/triggeredFunctions'), - triggeredFunctionsSelector: jest.fn().mockReturnValue({ + triggeredFunctionsLibrariesSelector: jest.fn().mockReturnValue({ loading: false, - libraries: null + data: null }), })) +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + let store: typeof mockedStore beforeEach(() => { cleanup() @@ -20,26 +31,7 @@ beforeEach(() => { store.clearActions() }) -const mockedLibraries = [ - { - name: 'lib1', - user: 'user1', - totalFunctions: 2, - pendingJobs: 1 - }, - { - name: 'lib2', - user: 'user1', - totalFunctions: 2, - pendingJobs: 1 - }, - { - name: 'lib3', - user: 'user2', - totalFunctions: 2, - pendingJobs: 1 - } -] +const mockedLibraries = TRIGGERED_FUNCTIONS_LIBRARIES_LIST_MOCKED_DATA describe('LibrariesPage', () => { it('should render', () => { @@ -54,8 +46,8 @@ describe('LibrariesPage', () => { }) it('should render message when no libraries uploaded', () => { - (triggeredFunctionsSelector as jest.Mock).mockReturnValueOnce({ - libraries: [], + (triggeredFunctionsLibrariesSelector as jest.Mock).mockReturnValueOnce({ + data: [], loading: false }) render() @@ -64,8 +56,8 @@ describe('LibrariesPage', () => { }) it('should render libraries list', () => { - (triggeredFunctionsSelector as jest.Mock).mockReturnValueOnce({ - libraries: mockedLibraries, + (triggeredFunctionsLibrariesSelector as jest.Mock).mockReturnValueOnce({ + data: mockedLibraries, loading: false }) render() @@ -76,8 +68,8 @@ describe('LibrariesPage', () => { }) it('should filter libraries list', () => { - (triggeredFunctionsSelector as jest.Mock).mockReturnValueOnce({ - libraries: mockedLibraries, + (triggeredFunctionsLibrariesSelector as jest.Mock).mockReturnValueOnce({ + data: mockedLibraries, loading: false }) render() @@ -105,8 +97,8 @@ describe('LibrariesPage', () => { }) it('should open library details', () => { - (triggeredFunctionsSelector as jest.Mock).mockReturnValueOnce({ - libraries: mockedLibraries, + (triggeredFunctionsLibrariesSelector as jest.Mock).mockReturnValueOnce({ + data: mockedLibraries, loading: false }) render() @@ -115,4 +107,22 @@ describe('LibrariesPage', () => { expect(screen.getByTestId('lib-details-lib1')).toBeInTheDocument() }) + + it('should call proper telemetry events', async () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + await act(() => { + render() + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARIES_RECEIVED, + eventData: { + databaseId: 'instanceId', + total: mockedLibraries.length + } + }) + }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.spec.tsx index 60cadfff2f..3f4bb933ac 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.spec.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.spec.tsx @@ -1,9 +1,11 @@ import React from 'react' import { cloneDeep } from 'lodash' +import reactRouterDom from 'react-router-dom' import { cleanup, mockedStore, render, screen, fireEvent, act } from 'uiSrc/utils/test-utils' import { getTriggeredFunctionsLibraryDetails, + setSelectedFunctionToShow, triggeredFunctionsSelectedLibrarySelector } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' @@ -11,7 +13,8 @@ import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA -} from 'uiSrc/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler' +} from 'uiSrc/mocks/data/triggeredFunctions' +import { FunctionType } from 'uiSrc/slices/interfaces/triggeredFunctions' import LibraryDetails from './LibraryDetails' jest.mock('uiSrc/telemetry', () => ({ @@ -33,6 +36,8 @@ beforeEach(() => { store.clearActions() }) +const mockedLibrary = TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA + describe('LibraryDetails', () => { it('should render', () => { expect(render()).toBeTruthy() @@ -57,20 +62,7 @@ describe('LibraryDetails', () => { (triggeredFunctionsSelectedLibrarySelector as jest.Mock).mockReturnValueOnce({ lastRefresh: null, loading: false, - data: { - apiVersion: '1.2', - code: 'code', - configuration: 'config', - functions: [ - { name: 'foo', type: 'functions' }, - { name: 'foo1', type: 'functions' }, - { name: 'foo2', type: 'cluster_functions' }, - { name: 'foo3', type: 'keyspace_triggers' }, - ], - name: 'lib', - pendingJobs: 12, - user: 'default', - } + data: mockedLibrary }) render() @@ -102,7 +94,15 @@ describe('LibraryDetails', () => { eventData: { databaseId: 'instanceId', apiVersion: TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA.apiVersion, - pendingJobs: TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA.pendingJobs + pendingJobs: TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA.pendingJobs, + configLoaded: true, + functions: { + cluster_functions: 1, + functions: 2, + keyspace_triggers: 1, + stream_triggers: 0, + total: 4 + } } }) @@ -139,4 +139,29 @@ describe('LibraryDetails', () => { } }) }) + + it('should call proper actions and history to move to the library', () => { + (triggeredFunctionsSelectedLibrarySelector as jest.Mock).mockReturnValueOnce({ + lastRefresh: null, + loading: false, + data: mockedLibrary + }) + + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + fireEvent.click(screen.getByTestId('moveToFunction-foo')) + + const expectedActions = [getTriggeredFunctionsLibraryDetails(), setSelectedFunctionToShow({ + library: 'lib', + name: 'foo', + type: 'functions' as FunctionType + })] + expect(store.getActions()).toEqual(expectedActions) + + expect(pushMock).toHaveBeenCalledTimes(1) + expect(pushMock).toHaveBeenCalledWith('/instanceId/triggered-functions/functions') + }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx index e324b9d906..3f2e9cc4bf 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx @@ -17,6 +17,7 @@ import { EuiLink } from '@elastic/eui' import cx from 'classnames' +import { isNil } from 'lodash' import { fetchTriggeredFunctionsLibrary, replaceTriggeredFunctionsLibraryAction, setSelectedFunctionToShow, @@ -67,7 +68,7 @@ const LibraryDetails = (props: Props) => { databaseId: instanceId, pendingJobs: lib?.pendingJobs || 0, apiVersion: lib?.apiVersion || '1.0', - configLoaded: lib?.code || false, + configLoaded: !isNil(lib?.code) || false, functions: { total: lib?.functions.length || 0, ...getFunctionsLengthByType(lib?.functions) diff --git a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts index 33da221fc2..010a02b735 100644 --- a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts +++ b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts @@ -2,26 +2,31 @@ import { cloneDeep } from 'lodash' import { AxiosError } from 'axios' import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' import reducer, { + fetchTriggeredFunctionsFunctionsList, fetchTriggeredFunctionsLibrariesList, - getTriggeredFunctionsLibrariesListFailure, + fetchTriggeredFunctionsLibrary, + getTriggeredFunctionsFunctionsList, + getTriggeredFunctionsFunctionsListFailure, + getTriggeredFunctionsFunctionsListSuccess, getTriggeredFunctionsLibrariesList, + getTriggeredFunctionsLibrariesListFailure, getTriggeredFunctionsLibrariesListSuccess, - initialState, - triggeredFunctionsSelector, getTriggeredFunctionsLibraryDetails, - getTriggeredFunctionsLibraryDetailsSuccess, getTriggeredFunctionsLibraryDetailsFailure, + getTriggeredFunctionsLibraryDetailsSuccess, + initialState, replaceTriggeredFunctionsLibrary, - replaceTriggeredFunctionsLibrarySuccess, + replaceTriggeredFunctionsLibraryAction, replaceTriggeredFunctionsLibraryFailure, - fetchTriggeredFunctionsLibrary, - replaceTriggeredFunctionsLibraryAction + replaceTriggeredFunctionsLibrarySuccess, + setSelectedFunctionToShow, + setSelectedLibraryToShow, + triggeredFunctionsSelector } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { apiService } from 'uiSrc/services' import { addErrorNotification } from 'uiSrc/slices/app/notifications' -import { - TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA -} from 'uiSrc/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler' +import { TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA } from 'uiSrc/mocks/data/triggeredFunctions' +import { FunctionType, TriggeredFunctionsFunction } from 'uiSrc/slices/interfaces/triggeredFunctions' let store: typeof mockedStore @@ -61,7 +66,10 @@ describe('triggeredFunctions slice', () => { // Arrange const state = { ...initialState, - loading: true + libraries: { + ...initialState.libraries, + loading: true + } } // Act @@ -81,8 +89,11 @@ describe('triggeredFunctions slice', () => { const libraries = [{ name: 'lib1', user: 'user1' }] const state = { ...initialState, - lastRefresh: Date.now(), - libraries + libraries: { + ...initialState.libraries, + lastRefresh: Date.now(), + data: libraries + } } // Act @@ -102,7 +113,10 @@ describe('triggeredFunctions slice', () => { const error = 'error' const state = { ...initialState, - error, + libraries: { + ...initialState.libraries, + error + } } // Act @@ -116,6 +130,127 @@ describe('triggeredFunctions slice', () => { }) }) + describe('getTriggeredFunctionsFunctionsList', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + functions: { + ...initialState.functions, + loading: true + } + } + + // Act + const nextState = reducer(initialState, getTriggeredFunctionsFunctionsList()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('getTriggeredFunctionsFunctionsListSuccess', () => { + it('should properly set state', () => { + // Arrange + const functions = [{ name: 'foo' }] + const state = { + ...initialState, + functions: { + ...initialState.functions, + lastRefresh: Date.now(), + data: functions + } + } + + // Act + const nextState = reducer(initialState, getTriggeredFunctionsFunctionsListSuccess(functions)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('getTriggeredFunctionsFunctionsListFailure', () => { + it('should properly set state', () => { + // Arrange + const error = 'error' + const state = { + ...initialState, + functions: { + ...initialState.functions, + error + } + } + + // Act + const nextState = reducer(initialState, getTriggeredFunctionsFunctionsListFailure(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('setSelectedFunctionToShow', () => { + it('should properly set state', () => { + // Arrange + const func: TriggeredFunctionsFunction = { + name: 'foo', + type: FunctionType.Function, + library: 'lib' + } + + const state = { + ...initialState, + functions: { + ...initialState.functions, + selected: func + } + } + + // Act + const nextState = reducer(initialState, setSelectedFunctionToShow(func)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('setSelectedLibraryToShow', () => { + it('should properly set state', () => { + // Arrange + const lib = 'lib' + + const state = { + ...initialState, + libraries: { + ...initialState.libraries, + selected: lib + } + } + + // Act + const nextState = reducer(initialState, setSelectedLibraryToShow(lib)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + describe('getTriggeredFunctionsLibraryDetails', () => { it('should properly set state', () => { // Arrange @@ -322,6 +457,56 @@ describe('triggeredFunctions slice', () => { }) }) + describe('fetchTriggeredFunctionsFunctionsList', () => { + it('succeed to fetch data', async () => { + const data: TriggeredFunctionsFunction[] = [ + { name: 'foo', library: 'lib', type: 'functions' as FunctionType } + ] + const responsePayload = { data, status: 200 } + + apiService.get = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch( + fetchTriggeredFunctionsFunctionsList('123') + ) + + // Assert + const expectedActions = [ + getTriggeredFunctionsFunctionsList(), + getTriggeredFunctionsFunctionsListSuccess(data), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch data', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.get = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch( + fetchTriggeredFunctionsFunctionsList('123') + ) + + // Assert + const expectedActions = [ + getTriggeredFunctionsFunctionsList(), + addErrorNotification(responsePayload as AxiosError), + getTriggeredFunctionsFunctionsListFailure(errorMessage) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) + describe('fetchTriggeredFunctionsLibrary', () => { it('succeed to fetch data', async () => { const data = TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA diff --git a/redisinsight/ui/src/utils/tests/triggered-functions/utils.spec.ts b/redisinsight/ui/src/utils/tests/triggered-functions/utils.spec.ts new file mode 100644 index 0000000000..159cf5117a --- /dev/null +++ b/redisinsight/ui/src/utils/tests/triggered-functions/utils.spec.ts @@ -0,0 +1,28 @@ +import { getFunctionsLengthByType } from 'uiSrc/utils/triggered-functions' +import { TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA } from 'uiSrc/mocks/data/triggeredFunctions' +import { FunctionType } from 'uiSrc/slices/interfaces/triggeredFunctions' + +describe('getFunctionsLengthByType', () => { + it('should properly return number of functions by type', () => { + expect(getFunctionsLengthByType(TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA)).toEqual({ + cluster_functions: 1, + functions: 2, + keyspace_triggers: 1, + stream_triggers: 1, + }) + + expect(getFunctionsLengthByType([{ name: '', type: FunctionType.ClusterFunction }])).toEqual({ + cluster_functions: 1, + functions: 0, + keyspace_triggers: 0, + stream_triggers: 0, + }) + + expect(getFunctionsLengthByType([])).toEqual({ + cluster_functions: 0, + functions: 0, + keyspace_triggers: 0, + stream_triggers: 0, + }) + }) +}) From 8d32c58db52f59df1a0afdb1c11445b0271d59fa Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Tue, 27 Jun 2023 20:45:26 +0200 Subject: [PATCH 022/106] add tests for RI-4591 --- .../components/monaco-editor/MonacoEditor.tsx | 1 + tests/e2e/helpers/constants.ts | 7 ++ .../pageObjects/my-redis-databases-page.ts | 5 +- .../triggers-and-functions-page.ts | 55 ++++++++++++++-- .../triggers-and-functions/libraries.e2e.ts | 64 ++++++++++++++++++- 5 files changed, 120 insertions(+), 12 deletions(-) diff --git a/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx index c3bdeca3b6..22c91f4522 100644 --- a/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx +++ b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx @@ -131,6 +131,7 @@ const MonacoEditor = (props: Props) => { color="secondary" onClick={() => setIsEditing(true)} className={styles.editBtn} + data-testid="edit-monaco-value" > diff --git a/tests/e2e/helpers/constants.ts b/tests/e2e/helpers/constants.ts index 9db95462f8..82a62491f6 100644 --- a/tests/e2e/helpers/constants.ts +++ b/tests/e2e/helpers/constants.ts @@ -47,3 +47,10 @@ export enum RecommendationIds { avoidLogicalDatabases = 'avoidLogicalDatabases', searchJson = 'searchJSON', } + +export enum LibrariesSections { + Functions = 'Functions', + KeyspaceTriggers = 'Keyspace', + ClusterFunctions = 'Cluster', + StreamFunctions= 'Stream', +} diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index 0ca1642ee3..3ffdbe21d3 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -18,7 +18,6 @@ export class MyRedisDatabasePage extends BasePage { //BUTTONS deleteDatabaseButton = Selector('[data-testid^=delete-instance-]'); confirmDeleteButton = Selector('[data-testid^=delete-instance-]').withExactText('Remove'); - deleteButtonInPopover = Selector('#deletePopover button'); confirmDeleteAllDbButton = Selector('[data-testid=delete-selected-dbs]'); editDatabaseButton = Selector('[data-testid^=edit-instance]'); @@ -39,7 +38,7 @@ export class MyRedisDatabasePage extends BasePage { exportSelectedDbsBtn = Selector('[data-testid=export-selected-dbs]'); //CHECKBOXES selectAllCheckbox = Selector('[data-test-subj=checkboxSelectAll]'); - exportPasswordsCheckbox = Selector('[data-testid=export-passwords]~div', {timeout: 500}); + exportPasswordsCheckbox = Selector('[data-testid=export-passwords]~div', { timeout: 500 }); //ICONS moduleColumn = Selector('[data-test-subj=tableHeaderCell_modules_3]'); moduleSearchIcon = Selector('[data-testid^=RediSearch]'); @@ -82,7 +81,7 @@ export class MyRedisDatabasePage extends BasePage { 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}); + await t.expect(db.exists).ok(`"${dbName}" database doesn't exist`, { timeout: 10000 }); await t.click(db); } diff --git a/tests/e2e/pageObjects/triggers-and-functions-page.ts b/tests/e2e/pageObjects/triggers-and-functions-page.ts index 258ff05bca..e669f09bc5 100644 --- a/tests/e2e/pageObjects/triggers-and-functions-page.ts +++ b/tests/e2e/pageObjects/triggers-and-functions-page.ts @@ -1,15 +1,22 @@ -import { Selector } from 'testcafe'; +import { Selector, t } from 'testcafe'; import { TriggersAndFunctionLibrary } from '../interfaces/triggers-and-functions'; +import { LibrariesSections } from '../helpers/constants'; import { InstancePage } from './instance-page'; export class TriggersAndFunctionsPage extends InstancePage { - //Containers - libraryRow = Selector('[data-testid=row-]'); + editMonacoButton = Selector('[data-testid=edit-monaco-value]'); + acceptButton = Selector('[data-testid=apply-btn]'); + + inputMonaco = Selector('[class=inlineMonacoEditor]'); + textAreaMonaco = Selector('[class^=view-lines]'); + + configurationLink = Selector('[data-testid=library-view-tab-config]'); + /** * Is library displayed in the table * @param libraryName The Library Name */ - getLibraryNameSelector(libraryName: string): Selector { + getLibraryNameSelector(libraryName: string): Selector { return Selector(`[data-testid=row-${libraryName}]`); } @@ -17,7 +24,7 @@ export class TriggersAndFunctionsPage extends InstancePage { * Get library item by name * @param libraryName The Library Name */ - async getLibraryItem(libraryName: string): Promise { + async getLibraryItem(libraryName: string): Promise { const item = {} as TriggersAndFunctionLibrary; const row = this.getLibraryNameSelector(libraryName); item.name = await row.find('span').nth(0).textContent; @@ -27,5 +34,41 @@ export class TriggersAndFunctionsPage extends InstancePage { return item; } -} + /** + * Is function displayed in the list + * @param functionsName The functions Name + * @param sectionName The section Name + */ + getFunctionsByName(sectionName: LibrariesSections, functionsName: string): Selector { + const KeySpaceSection = Selector(`[data-testid^=functions-${sectionName}]`); + return KeySpaceSection.find(`[data-testid=func-${functionsName}]`); + } + + /** + * Send commands in monacoEditor + * @param commandPart1 The command that should be on the first line + * @param commandPart2 command part except mandatory part + */ + async sendTextToMonaco(commandPart1: string, commandPart2?: string): Promise { + + await t + // remove text since replace doesn't work here + .pressKey('ctrl+a') + .pressKey('delete') + .typeText(this.inputMonaco, commandPart1); + if (commandPart2) { + await t.pressKey('enter') + .typeText(this.inputMonaco, commandPart2); + } + await t.click(this.acceptButton); + } + + /** + * Get text from monacoEditor + */ + async getTextToMonaco(): Promise { + + return (await this.textAreaMonaco.textContent).replace(/\s+/g, ' '); + } +} diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index 98a90f2f48..a2c97eb886 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -2,9 +2,9 @@ import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { BrowserPage, TriggersAndFunctionsPage } from '../../../pageObjects'; import { commonUrl, - ossStandaloneBigConfig, ossStandaloneRedisGears + ossStandaloneRedisGears } from '../../../helpers/conf'; -import { rte } from '../../../helpers/constants'; +import { rte, LibrariesSections } from '../../../helpers/constants'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { TriggersAndFunctionLibrary } from '../../../interfaces/triggers-and-functions'; @@ -17,7 +17,7 @@ fixture `Triggers and Functions` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisGears, ossStandaloneBigConfig.databaseName); + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisGears, ossStandaloneRedisGears.databaseName); }) .afterEach(async() => { // Delete database @@ -41,3 +41,61 @@ test await t.expect(row.totalFunctions).eql(item.totalFunctions, 'user name is unexpected'); }); +test.only + .after(async() => { + await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); + await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); + })('Verify that library details is displayed', async t => { + + const function1 = 'Function1'; + const function2 = 'function2'; + const asyncFunction = 'AsyncFunction'; + const stream = 'StreamTrigger'; + const cluster = 'registerClusterFunction'; + const keySpaceTrigger = 'registerKeySpaceTrigger'; + const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n + redis.registerFunction('${function1}', function(){}); + redis.registerFunction('${function2}', function(){}); + redis.registerAsyncFunction('${asyncFunction}', function(){}); + redis.registerStreamTrigger('${stream}', 'name', function(){}); + redis.registerClusterFunction('${cluster}', async function(){}); + redis.registerKeySpaceTrigger('${keySpaceTrigger}','',function(){});"`; + + await browserPage.Cli.sendCommandInCli(command); + + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(await triggersAndFunctionsPage.getLibraryNameSelector(libraryName)); + await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.Functions, function1).exists).ok('library is not displayed in Functions section'); + await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.Functions, function2).exists).ok('library is not displayed in Functions section'); + await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.Functions, asyncFunction).exists).ok('library is not displayed in Functions section'); + await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.StreamFunctions, stream).exists).ok('library is not displayed in Stream section'); + await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.ClusterFunctions, cluster).exists).ok('library is not displayed in cluster section'); + await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.KeyspaceTriggers, keySpaceTrigger).exists).ok('library is not displayed in key Space Trigger section'); + }); + +test + .after(async() => { + await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); + await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); + })('Verify that user can modify code', async t => { + const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n redis.registerFunction(\'foo\', ()=>{return \'bar\'});"`; + const commandUpdatedPart1 = `#!js api_version=1.0 name=${libraryName}`; + const commandUpdatedPart2 = ' redis.registerFunction(\'foo\', ()=>{return \'bar new\'});'; + const configuration = '{"redisgears_2.lock-redis-timeout": 1000}'; + + await browserPage.Cli.sendCommandInCli(command); + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(await triggersAndFunctionsPage.getLibraryNameSelector(libraryName)); + await t.click(triggersAndFunctionsPage.editMonacoButton); + await triggersAndFunctionsPage.sendTextToMonaco(commandUpdatedPart1, commandUpdatedPart2); + + await t.expect( + (await triggersAndFunctionsPage.getTextToMonaco())).eql(commandUpdatedPart1 + commandUpdatedPart2), 'code was not updated'; + + await t.click(await triggersAndFunctionsPage.configurationLink); + await t.click(triggersAndFunctionsPage.editMonacoButton); + await triggersAndFunctionsPage.sendTextToMonaco(configuration); + await t.expect( + (await triggersAndFunctionsPage.getTextToMonaco())).eql(configuration, 'configuration was not added'); + }); + From e775ea82136375c8dfb62e4803a27627b29904c6 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Wed, 28 Jun 2023 09:34:22 +0200 Subject: [PATCH 023/106] add tests for RI-4591 - comments fix --- .../triggers-and-functions-page.ts | 4 +- .../triggers-and-functions/libraries.e2e.ts | 40 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/e2e/pageObjects/triggers-and-functions-page.ts b/tests/e2e/pageObjects/triggers-and-functions-page.ts index e669f09bc5..cd79ee3535 100644 --- a/tests/e2e/pageObjects/triggers-and-functions-page.ts +++ b/tests/e2e/pageObjects/triggers-and-functions-page.ts @@ -37,8 +37,8 @@ export class TriggersAndFunctionsPage extends InstancePage { /** * Is function displayed in the list - * @param functionsName The functions Name - * @param sectionName The section Name + * @param sectionName The functions Name + * @param functionsName The section Name */ getFunctionsByName(sectionName: LibrariesSections, functionsName: string): Selector { const KeySpaceSection = Selector(`[data-testid^=functions-${sectionName}]`); diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index a2c97eb886..f879ac0a68 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -13,6 +13,15 @@ const triggersAndFunctionsPage = new TriggersAndFunctionsPage(); const libraryName = 'lib'; +const LIBRARIES_LIST = [ + { name: 'Function1', type: LibrariesSections.Functions }, + { name: 'function2', type: LibrariesSections.Functions }, + { name: 'AsyncFunction', type: LibrariesSections.Functions }, + { name: 'StreamTrigger', type: LibrariesSections.StreamFunctions }, + { name: 'ClusterFunction', type: LibrariesSections.ClusterFunctions }, + { name: 'keySpaceTrigger', type: LibrariesSections.KeyspaceTriggers }, +]; + fixture `Triggers and Functions` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) @@ -41,36 +50,27 @@ test await t.expect(row.totalFunctions).eql(item.totalFunctions, 'user name is unexpected'); }); -test.only +test .after(async() => { await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); })('Verify that library details is displayed', async t => { - - const function1 = 'Function1'; - const function2 = 'function2'; - const asyncFunction = 'AsyncFunction'; - const stream = 'StreamTrigger'; - const cluster = 'registerClusterFunction'; - const keySpaceTrigger = 'registerKeySpaceTrigger'; const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n - redis.registerFunction('${function1}', function(){}); - redis.registerFunction('${function2}', function(){}); - redis.registerAsyncFunction('${asyncFunction}', function(){}); - redis.registerStreamTrigger('${stream}', 'name', function(){}); - redis.registerClusterFunction('${cluster}', async function(){}); - redis.registerKeySpaceTrigger('${keySpaceTrigger}','',function(){});"`; + redis.registerFunction('${LIBRARIES_LIST[0]}', function(){}); + redis.registerFunction('${LIBRARIES_LIST[1]}', function(){}); + redis.registerAsyncFunction('${LIBRARIES_LIST[2]}', function(){}); + redis.registerStreamTrigger('${LIBRARIES_LIST[3]}', 'name', function(){}); + redis.registerClusterFunction('${LIBRARIES_LIST[4]}', async function(){}); + redis.registerKeySpaceTrigger('${LIBRARIES_LIST[5]}','',function(){});"`; await browserPage.Cli.sendCommandInCli(command); await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); await t.click(await triggersAndFunctionsPage.getLibraryNameSelector(libraryName)); - await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.Functions, function1).exists).ok('library is not displayed in Functions section'); - await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.Functions, function2).exists).ok('library is not displayed in Functions section'); - await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.Functions, asyncFunction).exists).ok('library is not displayed in Functions section'); - await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.StreamFunctions, stream).exists).ok('library is not displayed in Stream section'); - await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.ClusterFunctions, cluster).exists).ok('library is not displayed in cluster section'); - await t.expect(await triggersAndFunctionsPage.getFunctionsByName(LibrariesSections.KeyspaceTriggers, keySpaceTrigger).exists).ok('library is not displayed in key Space Trigger section'); + + for (const { name, type } of LIBRARIES_LIST) { + await t.expect(await triggersAndFunctionsPage.getFunctionsByName(type, name).exists).ok(`library is not displayed in ${type} section`); + } }); test From c31fba1f20e0678f8172a07a7aa38e05bcd8b3eb Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 28 Jun 2023 10:26:00 +0200 Subject: [PATCH 024/106] #RI-4581 - add context for last viewed page for triggers & functions pages --- .../TriggeredFunctionsPage.spec.tsx | 55 ++++++++++++++++++- .../TriggeredFunctionsPage.tsx | 23 ++++++-- redisinsight/ui/src/slices/app/context.ts | 11 +++- redisinsight/ui/src/slices/interfaces/app.ts | 21 ++++--- 4 files changed, 95 insertions(+), 15 deletions(-) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.spec.tsx index 350bdff215..c434d2b902 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.spec.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.spec.tsx @@ -1,11 +1,20 @@ import React from 'react' -import { BrowserRouter } from 'react-router-dom' +import reactRouterDom, { BrowserRouter } from 'react-router-dom' import { instance, mock } from 'ts-mockito' import { cloneDeep } from 'lodash' import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils' +import { Pages } from 'uiSrc/constants' +import { appContextTriggeredFunctions, setLastTriggeredFunctionsPage } from 'uiSrc/slices/app/context' import TriggeredFunctionsPage, { Props } from './TriggeredFunctionsPage' +jest.mock('uiSrc/slices/app/context', () => ({ + ...jest.requireActual('uiSrc/slices/app/context'), + appContextTriggeredFunctions: jest.fn().mockReturnValue({ + lastViewedPage: '', + }), +})) + const mockedProps = mock() let store: typeof mockedStore @@ -30,4 +39,48 @@ describe('TriggeredFunctionsPage', () => { ) ).toBeTruthy() }) + + it('should redirect to the functions by default', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.triggeredFunctions('instanceId') }) + + render( + + + + ) + + expect(pushMock).toBeCalledWith(Pages.triggeredFunctionsFunctions('instanceId')) + }) + + it('should redirect to the prev page from context', () => { + (appContextTriggeredFunctions as jest.Mock).mockReturnValueOnce({ + lastViewedPage: Pages.triggeredFunctionsLibraries('instanceId') + }) + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.triggeredFunctions('instanceId') }) + + render( + + + + ) + + expect(pushMock).toBeCalledWith(Pages.triggeredFunctionsLibraries('instanceId')) + }) + + it('should save proper page on unmount', () => { + reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.triggeredFunctionsLibraries('instanceId') }) + + const { unmount } = render( + + + + ) + + unmount() + expect(store.getActions()).toEqual([setLastTriggeredFunctionsPage(Pages.triggeredFunctionsLibraries('instanceId'))]) + }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx index 217b39e6a8..70e5af5b6a 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useState } from 'react' -import { useHistory } from 'react-router' -import { useLocation, useParams } from 'react-router-dom' -import { useSelector } from 'react-redux' +import { useLocation, useParams, useHistory } from 'react-router-dom' +import { useDispatch, useSelector } from 'react-redux' import { Pages } from 'uiSrc/constants' import InstanceHeader from 'uiSrc/components/instance-header' @@ -10,6 +9,10 @@ import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' import { appAnalyticsInfoSelector } from 'uiSrc/slices/app/info' +import { + appContextTriggeredFunctions, + setLastTriggeredFunctionsPage +} from 'uiSrc/slices/app/context' import TriggeredFunctionsPageRouter from './TriggeredFunctionsPageRouter' import TriggeredFunctionsTabs from './components/TriggeredFunctionsTabs' @@ -22,6 +25,7 @@ export interface Props { const TriggeredFunctionsPage = ({ routes = [] }: Props) => { const { identified: analyticsIdentified } = useSelector(appAnalyticsInfoSelector) const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) + const { lastViewedPage } = useSelector(appContextTriggeredFunctions) const [isPageViewSent, setIsPageViewSent] = useState(false) const pathnameRef = useRef('') @@ -29,17 +33,28 @@ const TriggeredFunctionsPage = ({ routes = [] }: Props) => { const { instanceId } = useParams<{ instanceId: string }>() const history = useHistory() const { pathname } = useLocation() + const dispatch = useDispatch() const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}` setTitle(`${dbName} - Triggers & Functions`) + useEffect(() => () => { + dispatch(setLastTriggeredFunctionsPage(pathnameRef.current)) + }, []) + useEffect(() => { if (pathname === Pages.triggeredFunctions(instanceId)) { - if (pathnameRef.current === Pages.triggeredFunctionsLibraries(instanceId)) { + if (pathnameRef.current && pathnameRef.current !== lastViewedPage) { history.push(pathnameRef.current) return } + // restore from context + if (lastViewedPage) { + history.push(lastViewedPage) + return + } + history.push(Pages.triggeredFunctionsFunctions(instanceId)) } diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index 1d72499790..d171ce5dd8 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -67,6 +67,9 @@ export const initialState: StateAppContext = { }, analytics: { lastViewedPage: '' + }, + triggeredFunctions: { + lastViewedPage: '' } } @@ -216,7 +219,10 @@ const appContextSlice = createSlice({ }, setDbIndexState: (state, { payload }: { payload: boolean }) => { state.dbIndex.disabled = payload - } + }, + setLastTriggeredFunctionsPage: (state, { payload }: { payload: string }) => { + state.triggeredFunctions.lastViewedPage = payload + }, }, }) @@ -253,6 +259,7 @@ export const { clearBrowserKeyListData, setDbIndexState, setRecommendationsShowHidden, + setLastTriggeredFunctionsPage, } = appContextSlice.actions // Selectors @@ -278,6 +285,8 @@ export const appContextAnalytics = (state: RootState) => state.app.context.analytics export const appContextDbIndex = (state: RootState) => state.app.context.dbIndex +export const appContextTriggeredFunctions = (state: RootState) => + state.app.context.triggeredFunctions // The reducer export default appContextSlice.reducer diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index 0bcc8a8bb9..bc9a1222d9 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -55,31 +55,31 @@ export interface StateAppContext { scrollRedisearchTopPosition: number isNotRendered: boolean selectedKey: Nullable - }, + } panelSizes: { [key: string]: number - }, + } tree: { delimiter: string panelSizes: { [key: string]: number - }, + } openNodes: { [key: string]: boolean - }, + } selectedLeaf: { [key: string]: { [key: string]: IKeyPropTypes } - }, - }, + } + } bulkActions: { opened: boolean }, keyDetailsSizes: { [key: string]: Nullable } - }, + } workbench: { script: string enablementArea: { @@ -92,14 +92,17 @@ export interface StateAppContext { [key: string]: number } } - }, + } pubsub: { channel: string message: string - }, + } analytics: { lastViewedPage: string } + triggeredFunctions: { + lastViewedPage: string + } } export interface StateAppRedisCommands { From 8d90ac7b9efdc07fcb3e1b112f82c490fd3e8bb1 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 28 Jun 2023 13:14:27 +0200 Subject: [PATCH 025/106] #RI-4685 - fix updating selected function --- .../pages/Functions/FunctionsPage.tsx | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx index bb251e83f3..1135a21286 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx @@ -4,7 +4,7 @@ import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiResiza import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import cx from 'classnames' -import { find } from 'lodash' +import { find, pick } from 'lodash' import { fetchTriggeredFunctionsFunctionsList, setSelectedFunctionToShow, @@ -42,28 +42,36 @@ const FunctionsPage = () => { }, [filterValue, functions]) const updateList = () => { - dispatch(fetchTriggeredFunctionsFunctionsList(instanceId, (functionsList) => { - if (selected) { - const findRow = find(functionsList, selected) + dispatch(fetchTriggeredFunctionsFunctionsList(instanceId, handleSuccessUpdateList)) + } - if (findRow) { - setSelectedRow(findRow) - } + const handleSuccessUpdateList = (data: TriggeredFunctionsFunction[]) => { + if (selectedRow) { + const pickFields = ['name', 'library', 'type'] + const findRow = find(data, pick(selectedRow, pickFields)) + setSelectedRow(findRow ?? null) + } + + if (selected) { + const findRow = find(data, selected) - dispatch(setSelectedFunctionToShow(null)) + if (findRow) { + setSelectedRow(findRow) } - sendEventTelemetry({ - event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTIONS_RECEIVED, - eventData: { - databaseId: instanceId, - functions: { - total: functionsList.length, - ...getFunctionsLengthByType(functionsList) - } + dispatch(setSelectedFunctionToShow(null)) + } + + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTIONS_RECEIVED, + eventData: { + databaseId: instanceId, + functions: { + total: data.length, + ...getFunctionsLengthByType(data) } - }) - })) + } + }) } const onChangeFiltering = (e: React.ChangeEvent) => { From 17c75c265b60f1e278c90229087785ce43547398 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 28 Jun 2023 13:21:30 +0200 Subject: [PATCH 026/106] update languages for monaco --- configs/webpack.config.renderer.dev.ts | 2 +- configs/webpack.config.renderer.prod.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configs/webpack.config.renderer.dev.ts b/configs/webpack.config.renderer.dev.ts index df91978331..85a5631685 100644 --- a/configs/webpack.config.renderer.dev.ts +++ b/configs/webpack.config.renderer.dev.ts @@ -232,7 +232,7 @@ const configuration: webpack.Configuration = { new ReactRefreshWebpackPlugin(), - new MonacoWebpackPlugin({ languages: ['json'], features: ['!rename'] }), + new MonacoWebpackPlugin({ languages: ['json', 'javascript', 'typescript'], features: ['!rename'] }), ...htmlPagesNames.map((htmlPageName) => ( new HtmlWebpackPlugin({ diff --git a/configs/webpack.config.renderer.prod.ts b/configs/webpack.config.renderer.prod.ts index 4a50ec9c7c..cfe710961b 100644 --- a/configs/webpack.config.renderer.prod.ts +++ b/configs/webpack.config.renderer.prod.ts @@ -180,7 +180,7 @@ const configuration: webpack.Configuration = { }, plugins: [ - new MonacoWebpackPlugin({ languages: ['json'], features: ['!rename'] }), + new MonacoWebpackPlugin({ languages: ['json', 'javascript', 'typescript'], features: ['!rename'] }), new webpack.EnvironmentPlugin({ NODE_ENV: 'production', From e00582ca5c1d931b4ac6aebed2302014758bb8cc Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Wed, 28 Jun 2023 15:47:53 +0300 Subject: [PATCH 027/106] Feature/ri 4583 delete library (#2256) * #RI-4583 - add delete library endpoint (#2248) --- .../dto/delete-library.dto.ts | 15 ++ .../modules/triggered-functions/dto/index.ts | 1 + .../triggered-functions.controller.ts | 16 +- .../triggered-functions.service.spec.ts | 39 ++++- .../triggered-functions.service.ts | 52 +++++-- .../notifications/success-messages.tsx | 12 +- redisinsight/ui/src/constants/api.ts | 1 + .../triggeredFunctionsHandler.ts | 9 +- .../pages/Libraries/LibrariesPage.tsx | 9 ++ .../DeleteLibrary/DeleteLibrary.spec.tsx | 122 +++++++++++++++ .../DeleteLibrary/DeleteLibrary.tsx | 104 +++++++++++++ .../components/DeleteLibrary/index.ts | 3 + .../DeleteLibrary/styles.module.scss | 3 + .../LibrariesList/LibrariesList.tsx | 58 +++++-- .../LibrariesList/styles.module.scss | 12 ++ .../LibraryDetails/LibraryDetails.tsx | 47 ++++-- .../LibraryDetails/styles.module.scss | 9 ++ .../slices/interfaces/triggeredFunctions.ts | 1 + .../triggeredFunctions.spec.ts | 142 +++++++++++++++++- .../triggeredFunctions/triggeredFunctions.ts | 57 ++++++- redisinsight/ui/src/telemetry/events.ts | 2 + 21 files changed, 672 insertions(+), 42 deletions(-) create mode 100644 redisinsight/api/src/modules/triggered-functions/dto/delete-library.dto.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/DeleteLibrary.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/DeleteLibrary.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/styles.module.scss diff --git a/redisinsight/api/src/modules/triggered-functions/dto/delete-library.dto.ts b/redisinsight/api/src/modules/triggered-functions/dto/delete-library.dto.ts new file mode 100644 index 0000000000..3b18c68b2e --- /dev/null +++ b/redisinsight/api/src/modules/triggered-functions/dto/delete-library.dto.ts @@ -0,0 +1,15 @@ +import { + IsNotEmpty, + IsString, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class DeleteLibraryDto { + @ApiProperty({ + description: 'Library name', + type: String, + }) + @IsString() + @IsNotEmpty() + libraryName: string; +} diff --git a/redisinsight/api/src/modules/triggered-functions/dto/index.ts b/redisinsight/api/src/modules/triggered-functions/dto/index.ts index 1152e6c8d2..a11a586445 100644 --- a/redisinsight/api/src/modules/triggered-functions/dto/index.ts +++ b/redisinsight/api/src/modules/triggered-functions/dto/index.ts @@ -1,2 +1,3 @@ export * from './library.dto'; export * from './upload-library.dto'; +export * from './delete-library.dto'; diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts index 68240c91c6..071a16f125 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts @@ -1,13 +1,14 @@ import { Get, Post, + Delete, Controller, UsePipes, ValidationPipe, Body, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; import { TriggeredFunctionsService } from 'src/modules/triggered-functions/triggered-functions.service'; import { ShortLibrary, Library, Function } from 'src/modules/triggered-functions/models'; -import { LibraryDto, UploadLibraryDto } from 'src/modules/triggered-functions/dto'; +import { LibraryDto, UploadLibraryDto, DeleteLibraryDto } from 'src/modules/triggered-functions/dto'; import { ClientMetadata } from 'src/common/models'; import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; @@ -95,4 +96,17 @@ export class TriggeredFunctionsController { ): Promise { return this.service.upload(clientMetadata, dto, true); } + + @Delete('/library') + @ApiRedisInstanceOperation({ + statusCode: 200, + description: 'Delete library by name', + }) + async deleteLibraries( + @BrowserClientMetadata() clientMetadata: ClientMetadata, + // library name probably can be really huge + @Body() dto: DeleteLibraryDto, + ): Promise { + return await this.service.delete(clientMetadata, dto.libraryName); + } } diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts index 6dd77ddd1b..6a796d5f3c 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts @@ -183,7 +183,6 @@ describe('TriggeredFunctionsService', () => { service.details(mockClientMetadata, mockLibraryName), ).rejects.toThrow(NotFoundException); }); - }); describe('libraryList', () => { @@ -308,4 +307,42 @@ describe('TriggeredFunctionsService', () => { } }); }); + + describe('delete', () => { + it('should delete library', async () => { + mockIORedisClient.sendCommand.mockResolvedValueOnce('OK'); + + expect(await service.delete(mockClientMetadata, mockLibraryName)).toEqual(undefined); + }); + + it('Should throw Error when error during creating a client in delete', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new Error()); + await service.delete(mockClientMetadata, mockLibraryName); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } + }); + + it('should handle acl error', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new Error('NOPERM')); + await service.delete(mockClientMetadata, mockLibraryName); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(ForbiddenException); + } + }); + + it('should handle HTTP error during deleting library', async () => { + try { + mockIORedisClient.sendCommand.mockRejectedValueOnce(new NotFoundException('Not Found')); + await service.delete(mockClientMetadata, mockLibraryName); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); + }); }); diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts index 116a357a33..4d2404ba0d 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts @@ -1,5 +1,7 @@ import { Command } from 'ioredis'; -import { HttpException, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { + HttpException, Injectable, Logger, NotFoundException, +} from '@nestjs/common'; import { catchAclError } from 'src/utils'; import { concat } from 'lodash'; import { plainToClass } from 'class-transformer'; @@ -124,30 +126,29 @@ export class TriggeredFunctionsService { * @param dto * @param isExist */ - async upload( + async upload( clientMetadata: ClientMetadata, dto: UploadLibraryDto, isExist = false, ): Promise { let client; - try { + try { const { code, configuration, } = dto; - - const commandArgs: any[] = isExist ? ['LOAD', 'REPLACE'] : ['LOAD']; - - if (configuration) { + const commandArgs: any[] = isExist ? ['LOAD', 'REPLACE'] : ['LOAD']; + + if (configuration) { commandArgs.push('CONFIG', configuration); - } + } - commandArgs.push(code); + commandArgs.push(code); client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); await client.sendCommand( new Command('TFUNCTION', [...commandArgs], { replyEncoding: 'utf8' }), ); - + this.logger.log('Succeed to upload library.'); return undefined; @@ -161,4 +162,35 @@ export class TriggeredFunctionsService { throw catchAclError(e); } } + + /** + * Delete triggered functions library + * @param clientMetadata + * @param libraryName + */ + async delete( + clientMetadata: ClientMetadata, + libraryName: string, + ): Promise { + let client; + try { + client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); + + await client.sendCommand( + new Command('TFUNCTION', ['DELETE', libraryName], { replyEncoding: 'utf8' }), + ); + + this.logger.log('Succeed to delete library.'); + + return undefined; + } catch (e) { + this.logger.error('Unable to delete library', e); + + if (e instanceof HttpException) { + throw e; + } + + throw catchAclError(e); + } + } } diff --git a/redisinsight/ui/src/components/notifications/success-messages.tsx b/redisinsight/ui/src/components/notifications/success-messages.tsx index 861f281e72..d9e56649ef 100644 --- a/redisinsight/ui/src/components/notifications/success-messages.tsx +++ b/redisinsight/ui/src/components/notifications/success-messages.tsx @@ -195,5 +195,15 @@ export default { ), className: 'dynamic' }) - } + }, + DELETE_LIBRARY: (libraryName: string) => ({ + title: 'Library has been deleted', + message: ( + <> + {formatNameShort(libraryName)} + {' '} + has been deleted. + + ), + }), } diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index 6289f71ed8..e0c83ae3a6 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -108,6 +108,7 @@ enum ApiEndpoints { TRIGGERED_FUNCTIONS_FUNCTIONS = 'triggered-functions/functions', TRIGGERED_FUNCTIONS_GET_LIBRARY = 'triggered-functions/get-library', TRIGGERED_FUNCTIONS_REPLACE_LIBRARY = 'triggered-functions/library/replace', + TRIGGERED_FUNCTIONS_LIBRARY = 'triggered-functions/library', NOTIFICATIONS = 'notifications', NOTIFICATIONS_READ = 'notifications/read', diff --git a/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts b/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts index 40febff0d5..08bce8f66d 100644 --- a/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts +++ b/redisinsight/ui/src/mocks/handlers/triggeredFunctions/triggeredFunctionsHandler.ts @@ -33,7 +33,14 @@ const handlers: RestHandler[] = [ ctx.status(200), ctx.json(TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA), ) - ) + ), + // delete library + rest.delete(getMswURL( + getUrl(INSTANCE_ID_MOCK, ApiEndpoints.TRIGGERED_FUNCTIONS_LIBRARY) + ), + async (_req, res, ctx) => res( + ctx.status(200), + )), ] export default handlers diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx index ad62798a20..d224c1af23 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx @@ -82,6 +82,13 @@ const LibrariesPage = () => { } } + const handleDelete = (name: string) => { + if (name === selectedRow) { + // clear selected library after delete + setSelectedRow(null) + } + } + const applyFiltering = () => { if (!filterValue) { setItems(libraries || []) @@ -167,6 +174,7 @@ const LibrariesPage = () => { lastRefresh={lastRefresh} selectedRow={selectedRow} onSelectRow={handleSelectRow} + onDeleteRow={handleDelete} /> )} {libraries?.length === 0 && ( @@ -197,6 +205,7 @@ const LibrariesPage = () => { )}
diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/DeleteLibrary.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/DeleteLibrary.spec.tsx new file mode 100644 index 0000000000..329eb7a4c7 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/DeleteLibrary.spec.tsx @@ -0,0 +1,122 @@ +import React from 'react' +import { mock } from 'ts-mockito' +import { cloneDeep } from 'lodash' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { cleanup, mockedStore, render, screen, fireEvent, act } from 'uiSrc/utils/test-utils' +import { + deleteTriggeredFunctionsLibrary, +} from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' + +import DeleteLibraryButton, { Props } from './DeleteLibrary' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +const mockedProps = mock() + +const mockLibrary = { + name: 'lib', + pendingJobs: 1, + user: 'default', + totalFunctions: 0, +} + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + instanceId: 'instanceId', + }), +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('DeleteLibraryButton', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call proper telemetry event', async () => { + const openPopover = jest.fn() + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render( + + ) + + await act(() => { + fireEvent.click(screen.getByTestId('delete-library-icon-lib')) + }) + + expect(openPopover).toBeCalledWith('lib') + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DELETE_CLICKED, + eventData: { + databaseId: 'instanceId' + } + }) + }) + + it('should call proper telemetry event', async () => { + const closePopover = jest.fn() + const onDelete = jest.fn() + + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render( + + ) + + await act(() => { + fireEvent.click(screen.getByTestId('delete-library-lib')) + }) + + expect(closePopover).toHaveBeenCalled() + expect(sendEventTelemetry).toHaveBeenCalledTimes(2) + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DELETE_CLICKED, + eventData: { + databaseId: 'instanceId' + } + }) + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DELETED, + eventData: { + databaseId: 'instanceId', + pendingJobs: 1, + } + }) + }) + + it('should call deleteTriggeredFunctionsLibrary', async () => { + const closePopover = jest.fn() + + const expectedActions = [deleteTriggeredFunctionsLibrary()] + + render( + + ) + + act(() => { + fireEvent.click(screen.getByTestId('delete-library-lib')) + }) + + expect(store.getActions()).toEqual(expectedActions) + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/DeleteLibrary.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/DeleteLibrary.tsx new file mode 100644 index 0000000000..f30fdcb574 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/DeleteLibrary.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { EuiButton, EuiButtonIcon, EuiPopover, EuiText, EuiSpacer } from '@elastic/eui' + +import { deleteTriggeredFunctionsLibraryAction, triggeredFunctionsLibrariesSelector } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { TriggeredFunctionsLibrary, TriggeredFunctionsLibraryDetails } from 'uiSrc/slices/interfaces/triggeredFunctions' +import { formatLongName, Nullable } from 'uiSrc/utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' + +import styles from './styles.module.scss' + +export interface Props { + library: Nullable | Nullable + isOpen: boolean + openPopover: (library: string) => void + onDelete: (name: string) => void + closePopover: () => void +} + +const DeleteLibraryButton = (props: Props) => { + const { library, onDelete, isOpen, closePopover, openPopover } = props + + const { deleting } = useSelector(triggeredFunctionsLibrariesSelector) + + const { instanceId } = useParams<{ instanceId: string }>() + + const dispatch = useDispatch() + const { name = '', pendingJobs = 0 } = library || {} + + const handleClickDelete = () => { + openPopover(name) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DELETE_CLICKED, + eventData: { + databaseId: instanceId, + } + }) + } + + const onSuccess = (name: string) => { + onDelete(name) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_DELETED, + eventData: { + databaseId: instanceId, + pendingJobs, + } + }) + } + + const handleDelete = () => { + dispatch(deleteTriggeredFunctionsLibraryAction(instanceId, name, onSuccess)) + closePopover() + } + + return ( + + )} + onClick={(e) => e.stopPropagation()} + data-testid={`delete-library-popover-${name}`} + > +
+ +

+ {formatLongName(name)} +

+ + and all its functions will be deleted. + +
+ + + Delete + +
+
+ ) +} + +export default DeleteLibraryButton diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/index.ts new file mode 100644 index 0000000000..25c55a8bac --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/index.ts @@ -0,0 +1,3 @@ +import DeleteLibraryButton from './DeleteLibrary' + +export default DeleteLibraryButton diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/styles.module.scss new file mode 100644 index 0000000000..586735d8cc --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary/styles.module.scss @@ -0,0 +1,3 @@ +.deletePopover { + max-width: 400px !important; +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx index 0835d11026..7cc9db1c8d 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx @@ -1,10 +1,17 @@ import React, { useState } from 'react' -import { EuiBasicTableColumn, EuiInMemoryTable, EuiText, EuiToolTip, PropertySort } from '@elastic/eui' +import { + EuiBasicTableColumn, + EuiInMemoryTable, + EuiText, + EuiToolTip, + PropertySort, +} from '@elastic/eui' import cx from 'classnames' import { useParams } from 'react-router-dom' -import { Maybe, Nullable } from 'uiSrc/utils' +import { Maybe, Nullable, formatLongName } from 'uiSrc/utils' import AutoRefresh from 'uiSrc/pages/browser/components/auto-refresh' +import DeleteLibraryButton from 'uiSrc/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { TriggeredFunctionsLibrary } from 'uiSrc/slices/interfaces/triggeredFunctions' import styles from './styles.module.scss' @@ -16,13 +23,15 @@ export interface Props { lastRefresh: Nullable selectedRow: Nullable onSelectRow: (name: string) => void + onDeleteRow: (name: string) => void } const NoLibrariesMessage: React.ReactNode = (No Libraries found) const LibrariesList = (props: Props) => { - const { items, loading, onRefresh, lastRefresh, selectedRow, onSelectRow } = props + const { items, loading, onRefresh, lastRefresh, selectedRow, onSelectRow, onDeleteRow } = props const [sort, setSort] = useState>(undefined) + const [popover, setPopover] = useState>(undefined) const { instanceId } = useParams<{ instanceId: string }>() @@ -33,14 +42,18 @@ const LibrariesList = (props: Props) => { sortable: true, truncateText: true, width: '25%', - render: (value: string) => ( - - <>{value} - - ) + render: (value: string) => { + const tooltipContent = formatLongName(value) + + return ( + + <>{value} + + ) + } }, { name: 'Username', @@ -74,10 +87,30 @@ const LibrariesList = (props: Props) => { { name: '', field: 'actions', - width: '20%' + align: 'right', + width: '20%', + render: (_act: any, library: TriggeredFunctionsLibrary) => ( +
+ +
+ ) }, ] + const handleDeleteClick = (library: string) => { + setPopover(library) + } + + const handleClosePopover = () => { + setPopover(undefined) + } + const handleSelect = (item: TriggeredFunctionsLibrary) => { onSelectRow(item.name) } @@ -143,6 +176,7 @@ const LibrariesList = (props: Props) => { })} message={NoLibrariesMessage} onTableChange={handleSorting} + onWheel={handleClosePopover} className={cx('inMemoryTableDefault', 'noBorders', 'triggeredFunctions__table')} data-testid="libraries-list-table" /> diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss index 3d5c1d8427..5939924a84 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/styles.module.scss @@ -36,3 +36,15 @@ } } } + +.deleteBtn { + visibility: hidden; + + &.show { + visibility: visible; + } +} + +:global(.euiTableRow:hover) .deleteBtn { + visibility: visible; +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx index 3f2e9cc4bf..1e70bf4e2f 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibraryDetails/LibraryDetails.tsx @@ -25,11 +25,13 @@ import { } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { MonacoJS, MonacoJson } from 'uiSrc/components/monaco-editor' +import DeleteLibraryButton from 'uiSrc/pages/triggeredFunctions/pages/Libraries/components/DeleteLibrary' import { reSerializeJSON } from 'uiSrc/utils/formatters/json' import { FunctionType } from 'uiSrc/slices/interfaces/triggeredFunctions' import AutoRefresh from 'uiSrc/pages/browser/components/auto-refresh' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { formatLongName, Nullable } from 'uiSrc/utils' import { LIB_DETAILS_TABS, @@ -43,15 +45,17 @@ import styles from './styles.module.scss' export interface Props { name: string onClose: () => void + onDeleteRow: (name: string) => void } const LibraryDetails = (props: Props) => { - const { name, onClose } = props + const { name, onClose, onDeleteRow } = props const { loading, lastRefresh, data: library } = useSelector(triggeredFunctionsSelectedLibrarySelector) const [selectedView, setSelectedView] = useState(LIB_DETAILS_TABS[0].id) const [configuration, setConfiguration] = useState('_') const [code, setCode] = useState('_') + const [popover, setPopover] = useState>(null) const { instanceId } = useParams<{ instanceId: string }>() const history = useHistory() @@ -136,6 +140,14 @@ const LibraryDetails = (props: Props) => { setCode(library?.code ?? '') } + const handleDeleteClick = (library: string) => { + setPopover(library) + } + + const handleClosePopover = () => { + setPopover(null) + } + const goToFunction = ( e: React.MouseEvent, { functionName, type }: { functionName: string, type: FunctionType } @@ -209,7 +221,7 @@ const LibraryDetails = (props: Props) => {
{name} @@ -220,17 +232,26 @@ const LibraryDetails = (props: Props) => { {library?.apiVersion && (API: {library.apiVersion})} - +
+ + +
error: string selected: Nullable + deleting: boolean } functions: { data: Nullable diff --git a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts index 010a02b735..81757210a0 100644 --- a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts +++ b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts @@ -21,10 +21,15 @@ import reducer, { replaceTriggeredFunctionsLibrarySuccess, setSelectedFunctionToShow, setSelectedLibraryToShow, - triggeredFunctionsSelector + triggeredFunctionsSelector, + deleteTriggeredFunctionsLibrary, + deleteTriggeredFunctionsLibrarySuccess, + deleteTriggeredFunctionsLibraryFailure, + deleteTriggeredFunctionsLibraryAction, } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { apiService } from 'uiSrc/services' -import { addErrorNotification } from 'uiSrc/slices/app/notifications' +import { addMessageNotification, addErrorNotification } from 'uiSrc/slices/app/notifications' +import successMessages from 'uiSrc/components/notifications/success-messages' import { TRIGGERED_FUNCTIONS_LIB_DETAILS_MOCKED_DATA } from 'uiSrc/mocks/data/triggeredFunctions' import { FunctionType, TriggeredFunctionsFunction } from 'uiSrc/slices/interfaces/triggeredFunctions' @@ -404,6 +409,91 @@ describe('triggeredFunctions slice', () => { }) expect(triggeredFunctionsSelector(rootState)).toEqual(state) }) + + describe('deleteTriggeredFunctionsLibrary', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + libraries: { + ...initialState.libraries, + deleting: true + } + } + + // Act + const nextState = reducer(initialState, deleteTriggeredFunctionsLibrary()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('deleteTriggeredFunctionsLibrarySuccess', () => { + it('should properly set state', () => { + const libraries = [ + { name: 'lib1', user: 'user1', pendingJobs: 0, totalFunctions: 0 }, + ] + // Arrange + const currentState = { + ...initialState, + libraries: { + ...initialState.libraries, + data: libraries, + deleting: true, + }, + } + const state = { + ...initialState, + libraries: { + ...initialState.libraries, + data: [], + deleting: false, + }, + } + + // Act + const nextState = reducer(currentState, deleteTriggeredFunctionsLibrarySuccess('lib1')) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('deleteTriggeredFunctionsLibraryFailure', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + libraries: { + ...initialState.libraries, + deleting: true, + }, + } + const state = { + ...initialState, + libraries: { + ...initialState.libraries, + deleting: false, + }, + } + + // Act + const nextState = reducer(currentState, deleteTriggeredFunctionsLibraryFailure()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) }) // thunks @@ -601,5 +691,53 @@ describe('triggeredFunctions slice', () => { expect(store.getActions()).toEqual(expectedActions) }) }) + + describe('deleteTriggeredFunctionsLibraryAction', () => { + it('succeed to delete libraries', async () => { + const responsePayload = { status: 200 } + + apiService.delete = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch( + deleteTriggeredFunctionsLibraryAction('instanceId', 'name') + ) + + // Assert + const expectedActions = [ + deleteTriggeredFunctionsLibrary(), + deleteTriggeredFunctionsLibrarySuccess('name'), + addMessageNotification(successMessages.DELETE_LIBRARY('name')) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to delete libraries', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.delete = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch( + deleteTriggeredFunctionsLibraryAction('instanceId', 'name') + ) + + // Assert + const expectedActions = [ + deleteTriggeredFunctionsLibrary(), + addErrorNotification(responsePayload as AxiosError), + deleteTriggeredFunctionsLibraryFailure() + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) }) }) diff --git a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts index a080263321..5d8f8e2db0 100644 --- a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts +++ b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts @@ -1,5 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AxiosError } from 'axios' +import { remove } from 'lodash' +import successMessages from 'uiSrc/components/notifications/success-messages' import { StateTriggeredFunctions, TriggeredFunctionsFunction, @@ -10,7 +12,7 @@ import { AppDispatch, RootState } from 'uiSrc/slices/store' import { apiService } from 'uiSrc/services' import { getApiErrorMessage, getUrl, isStatusSuccessful, Nullable } from 'uiSrc/utils' import { ApiEndpoints } from 'uiSrc/constants' -import { addErrorNotification } from 'uiSrc/slices/app/notifications' +import { addMessageNotification, addErrorNotification } from 'uiSrc/slices/app/notifications' export const initialState: StateTriggeredFunctions = { libraries: { @@ -19,6 +21,7 @@ export const initialState: StateTriggeredFunctions = { lastRefresh: null, error: '', selected: null, + deleting: false, }, functions: { data: null, @@ -88,6 +91,19 @@ const triggeredFunctionsSlice = createSlice({ replaceTriggeredFunctionsLibraryFailure: (state) => { state.selectedLibrary.loading = false }, + // delete library + deleteTriggeredFunctionsLibrary: (state) => { + state.libraries.deleting = true + }, + deleteTriggeredFunctionsLibrarySuccess: (state, { payload }) => { + state.libraries.deleting = false + if (state.libraries.data) { + remove(state.libraries.data, (library) => library.name === payload) + } + }, + deleteTriggeredFunctionsLibraryFailure: (state) => { + state.libraries.deleting = false + }, setSelectedFunctionToShow: (state, { payload }: PayloadAction>) => { state.functions.selected = payload }, @@ -112,6 +128,9 @@ export const { replaceTriggeredFunctionsLibrary, replaceTriggeredFunctionsLibrarySuccess, replaceTriggeredFunctionsLibraryFailure, + deleteTriggeredFunctionsLibrary, + deleteTriggeredFunctionsLibrarySuccess, + deleteTriggeredFunctionsLibraryFailure, setSelectedFunctionToShow, setSelectedLibraryToShow, } = triggeredFunctionsSlice.actions @@ -251,3 +270,39 @@ export function replaceTriggeredFunctionsLibraryAction( } } } + +export function deleteTriggeredFunctionsLibraryAction( + instanceId: string, + libraryName: string, + onSuccessAction?: (library: string) => void, + onFailAction?: () => void, +) { + return async (dispatch: AppDispatch) => { + try { + dispatch(deleteTriggeredFunctionsLibrary()) + + const { status } = await apiService.delete( + getUrl( + instanceId, + ApiEndpoints.TRIGGERED_FUNCTIONS_LIBRARY, + ), + { + data: { libraryName }, + } + ) + + if (isStatusSuccessful(status)) { + dispatch(deleteTriggeredFunctionsLibrarySuccess(libraryName)) + dispatch( + addMessageNotification(successMessages.DELETE_LIBRARY(libraryName)) + ) + onSuccessAction?.(libraryName) + } + } catch (_err) { + const error = _err as AxiosError + dispatch(addErrorNotification(error)) + dispatch(deleteTriggeredFunctionsLibraryFailure()) + onFailAction?.() + } + } +} diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index cf06714d0d..3e498c9471 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -249,6 +249,8 @@ export enum TelemetryEvent { TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_DISABLED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_DETAILS_AUTO_REFRESH_DISABLED', TRIGGERS_AND_FUNCTIONS_LIBRARY_CODE_REPLACED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_CODE_REPLACED', TRIGGERS_AND_FUNCTIONS_LIBRARY_CONFIGURATION_REPLACED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_CONFIGURATION_REPLACED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_DELETE_CLICKED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_DELETE_CLICKED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_DELETED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_DELETED', TRIGGERS_AND_FUNCTIONS_LIBRARIES_CLICKED = 'TRIGGERS_AND_FUNCTIONS_LIBRARIES_CLICKED', TRIGGERS_AND_FUNCTIONS_FUNCTIONS_CLICKED = 'TRIGGERS_AND_FUNCTIONS_FUNCTIONS_CLICKED', TRIGGERS_AND_FUNCTIONS_FUNCTIONS_SORTED = 'TRIGGERS_AND_FUNCTIONS_FUNCTIONS_SORTED', From 9a946cde13617c401536db6ad24cf8fd2fc7f381 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Thu, 29 Jun 2023 11:59:31 +0200 Subject: [PATCH 028/106] add tests for RI-4581 --- .../triggers-and-functions-functions-page.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/e2e/pageObjects/triggers-and-functions-functions-page.ts diff --git a/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts b/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts new file mode 100644 index 0000000000..b59fd19e31 --- /dev/null +++ b/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts @@ -0,0 +1,28 @@ +import { Selector } from 'testcafe'; +import { FunctionsSections } from '../helpers/constants'; +import { InstancePage } from './instance-page'; +export class TriggersAndFunctionsFunctionsPage extends InstancePage { + + librariesLink = Selector('[data-testid=triggered-functions-tab-libraries]'); + + //Masks + // insert name + functionNameMask = '[data-testid=row-$name]'; + sectionMask = '[data-testid^=function-details-$name]'; + + /** + * Is functions displayed in the table + * @param name The functions Name + */ + getFunctionsNameSelector(name: string): Selector { + return Selector(this.functionNameMask.replace(/\$name/g, name)); + } + + /** + * Is function displayed in the list + * @param sectionName The functions Name + */ + async getFieldsAndValuesBySection(sectionName: FunctionsSections): Promise { + return Selector(this.sectionMask.replace(/\$name/g, sectionName)).textContent; + } +} From c912e364618231885f16d467cee7cb17e3b73c70 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Thu, 29 Jun 2023 11:59:52 +0200 Subject: [PATCH 029/106] add tests for RI-4581 --- ...nctions-page.ts => triggers-and-functions-libraries-page.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/e2e/pageObjects/{triggers-and-functions-page.ts => triggers-and-functions-libraries-page.ts} (97%) diff --git a/tests/e2e/pageObjects/triggers-and-functions-page.ts b/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts similarity index 97% rename from tests/e2e/pageObjects/triggers-and-functions-page.ts rename to tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts index cd79ee3535..8e913ab77c 100644 --- a/tests/e2e/pageObjects/triggers-and-functions-page.ts +++ b/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts @@ -3,7 +3,7 @@ import { TriggersAndFunctionLibrary } from '../interfaces/triggers-and-functions import { LibrariesSections } from '../helpers/constants'; import { InstancePage } from './instance-page'; -export class TriggersAndFunctionsPage extends InstancePage { +export class TriggersAndFunctionsLibrariesPage extends InstancePage { editMonacoButton = Selector('[data-testid=edit-monaco-value]'); acceptButton = Selector('[data-testid=apply-btn]'); From 33d15d7f939de54114bd5cb60fc042985ba7e00d Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Thu, 29 Jun 2023 12:00:56 +0200 Subject: [PATCH 030/106] add tests for RI-4581 #2 --- tests/e2e/helpers/constants.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e/helpers/constants.ts b/tests/e2e/helpers/constants.ts index 82a62491f6..3fbd120364 100644 --- a/tests/e2e/helpers/constants.ts +++ b/tests/e2e/helpers/constants.ts @@ -54,3 +54,8 @@ export enum LibrariesSections { ClusterFunctions = 'Cluster', StreamFunctions= 'Stream', } + +export enum FunctionsSections { + General = 'General', + Flag = 'Flag' +} From 975ae17439ede50e4e3b933fe570165854a642a8 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Thu, 29 Jun 2023 12:01:21 +0200 Subject: [PATCH 031/106] add tests for RI-4581 #3 --- tests/e2e/pageObjects/index.ts | 6 +- .../triggers-and-functions/libraries.e2e.ts | 68 +++++++++++++------ 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/tests/e2e/pageObjects/index.ts b/tests/e2e/pageObjects/index.ts index 6d800118c0..1436a687ca 100644 --- a/tests/e2e/pageObjects/index.ts +++ b/tests/e2e/pageObjects/index.ts @@ -9,7 +9,8 @@ import { PubSubPage } from './pub-sub-page'; import { SlowLogPage } from './slow-log-page'; import { BasePage } from './base-page'; import { InstancePage } from './instance-page'; -import { TriggersAndFunctionsPage } from './triggers-and-functions-page'; +import { TriggersAndFunctionsLibrariesPage } from './triggers-and-functions-libraries-page'; +import { TriggersAndFunctionsFunctionsPage } from './triggers-and-functions-functions-page'; export { AutoDiscoverREDatabases, @@ -23,5 +24,6 @@ export { SlowLogPage, BasePage, InstancePage, - TriggersAndFunctionsPage + TriggersAndFunctionsLibrariesPage, + TriggersAndFunctionsFunctionsPage }; diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index f879ac0a68..352dc755ff 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -1,15 +1,17 @@ import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { BrowserPage, TriggersAndFunctionsPage } from '../../../pageObjects'; +import { BrowserPage, TriggersAndFunctionsLibrariesPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneRedisGears } from '../../../helpers/conf'; -import { rte, LibrariesSections } from '../../../helpers/constants'; +import { rte, LibrariesSections, FunctionsSections } from '../../../helpers/constants'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { TriggersAndFunctionLibrary } from '../../../interfaces/triggers-and-functions'; +import { TriggersAndFunctionsFunctionsPage } from '../../../pageObjects/triggers-and-functions-functions-page'; const browserPage = new BrowserPage(); -const triggersAndFunctionsPage = new TriggersAndFunctionsPage(); +const triggersAndFunctionsLibrariesPage = new TriggersAndFunctionsLibrariesPage(); +const triggersAndFunctionsFunctionsPage = new TriggersAndFunctionsFunctionsPage(); const libraryName = 'lib'; @@ -19,7 +21,7 @@ const LIBRARIES_LIST = [ { name: 'AsyncFunction', type: LibrariesSections.Functions }, { name: 'StreamTrigger', type: LibrariesSections.StreamFunctions }, { name: 'ClusterFunction', type: LibrariesSections.ClusterFunctions }, - { name: 'keySpaceTrigger', type: LibrariesSections.KeyspaceTriggers }, + { name: 'keySpaceTrigger', type: LibrariesSections.KeyspaceTriggers } ]; fixture `Triggers and Functions` @@ -43,7 +45,8 @@ test const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n redis.registerFunction(\'foo\', ()=>{return \'bar\'})"`; await browserPage.Cli.sendCommandInCli(command); await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); - const row = await triggersAndFunctionsPage.getLibraryItem(libraryName); + await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + const row = await triggersAndFunctionsLibrariesPage.getLibraryItem(libraryName); await t.expect(row.name).eql(item.name, 'library name is unexpected'); await t.expect(row.user).eql(item.user, 'user name is unexpected'); await t.expect(row.pending).eql(item.pending, 'user name is unexpected'); @@ -56,20 +59,21 @@ test await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); })('Verify that library details is displayed', async t => { const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n - redis.registerFunction('${LIBRARIES_LIST[0]}', function(){}); - redis.registerFunction('${LIBRARIES_LIST[1]}', function(){}); - redis.registerAsyncFunction('${LIBRARIES_LIST[2]}', function(){}); - redis.registerStreamTrigger('${LIBRARIES_LIST[3]}', 'name', function(){}); - redis.registerClusterFunction('${LIBRARIES_LIST[4]}', async function(){}); - redis.registerKeySpaceTrigger('${LIBRARIES_LIST[5]}','',function(){});"`; + redis.registerFunction('${LIBRARIES_LIST[0].name}', function(){}); + redis.registerFunction('${LIBRARIES_LIST[1].name}', function(){}); + redis.registerAsyncFunction('${LIBRARIES_LIST[2].name}', function(){}); + redis.registerStreamTrigger('${LIBRARIES_LIST[3].name}', 'name', function(){}); + redis.registerClusterFunction('${LIBRARIES_LIST[4].name}', async function(){}); + redis.registerKeySpaceTrigger('${LIBRARIES_LIST[5].name}','',function(){});"`; await browserPage.Cli.sendCommandInCli(command); await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); - await t.click(await triggersAndFunctionsPage.getLibraryNameSelector(libraryName)); + await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + await t.click(await triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName)); for (const { name, type } of LIBRARIES_LIST) { - await t.expect(await triggersAndFunctionsPage.getFunctionsByName(type, name).exists).ok(`library is not displayed in ${type} section`); + await t.expect(await triggersAndFunctionsLibrariesPage.getFunctionsByName(type, name).exists).ok(`library is not displayed in ${type} section`); } }); @@ -85,17 +89,39 @@ test await browserPage.Cli.sendCommandInCli(command); await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); - await t.click(await triggersAndFunctionsPage.getLibraryNameSelector(libraryName)); - await t.click(triggersAndFunctionsPage.editMonacoButton); - await triggersAndFunctionsPage.sendTextToMonaco(commandUpdatedPart1, commandUpdatedPart2); + await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + await t.click(await triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName)); + await t.click(triggersAndFunctionsLibrariesPage.editMonacoButton); + await triggersAndFunctionsLibrariesPage.sendTextToMonaco(commandUpdatedPart1, commandUpdatedPart2); await t.expect( - (await triggersAndFunctionsPage.getTextToMonaco())).eql(commandUpdatedPart1 + commandUpdatedPart2), 'code was not updated'; + (await triggersAndFunctionsLibrariesPage.getTextToMonaco())).eql(commandUpdatedPart1 + commandUpdatedPart2), 'code was not updated'; - await t.click(await triggersAndFunctionsPage.configurationLink); - await t.click(triggersAndFunctionsPage.editMonacoButton); - await triggersAndFunctionsPage.sendTextToMonaco(configuration); + await t.click(await triggersAndFunctionsLibrariesPage.configurationLink); + await t.click(triggersAndFunctionsLibrariesPage.editMonacoButton); + await triggersAndFunctionsLibrariesPage.sendTextToMonaco(configuration); await t.expect( - (await triggersAndFunctionsPage.getTextToMonaco())).eql(configuration, 'configuration was not added'); + (await triggersAndFunctionsLibrariesPage.getTextToMonaco())).eql(configuration, 'configuration was not added'); + }); + +test.only + .after(async() => { + await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); + await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); + })('Verify that function details is displayed', async t => { + const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n + redis.registerAsyncFunction('${LIBRARIES_LIST[2].name}', function(client){ + return client.call('ping');},{flags: [redis.functionFlags.RAW_ARGUMENTS]});"`; + const functionDetails = { libraryName: libraryName, isAsync: 'isAsync:Yes', flag: 'raw-arguments' }; + + await browserPage.Cli.sendCommandInCli(command); + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(await triggersAndFunctionsFunctionsPage.getFunctionsNameSelector(LIBRARIES_LIST[2].name)); + let fieldsAndValue = await triggersAndFunctionsFunctionsPage.getFieldsAndValuesBySection(FunctionsSections.General); + await t.expect(fieldsAndValue).contains(functionDetails.libraryName, 'library name is not corrected'); + await t.expect(fieldsAndValue).contains(functionDetails.isAsync, 'async is not corrected'); + + fieldsAndValue = await triggersAndFunctionsFunctionsPage.getFieldsAndValuesBySection(FunctionsSections.Flag); + await t.expect(fieldsAndValue).contains(functionDetails.flag, 'flag name is not displayed'); }); From fc9a9e146bfff44693aaddc0321c91ce91b19893 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Thu, 29 Jun 2023 12:02:43 +0200 Subject: [PATCH 032/106] add tests for RI-4581 #4 --- .../tests/critical-path/triggers-and-functions/libraries.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index 352dc755ff..51b1b1cef6 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -104,7 +104,7 @@ test (await triggersAndFunctionsLibrariesPage.getTextToMonaco())).eql(configuration, 'configuration was not added'); }); -test.only +test .after(async() => { await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); From b318a4964486c3745c8f6fca7a0bcc25500c3a64 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Thu, 29 Jun 2023 12:24:20 +0200 Subject: [PATCH 033/106] add tests for RI-4581 - comments fix --- .../pageObjects/triggers-and-functions-functions-page.ts | 8 ++++---- .../critical-path/triggers-and-functions/libraries.e2e.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts b/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts index b59fd19e31..cddccfb75c 100644 --- a/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts +++ b/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts @@ -11,7 +11,7 @@ export class TriggersAndFunctionsFunctionsPage extends InstancePage { sectionMask = '[data-testid^=function-details-$name]'; /** - * Is functions displayed in the table + * get function by name * @param name The functions Name */ getFunctionsNameSelector(name: string): Selector { @@ -19,10 +19,10 @@ export class TriggersAndFunctionsFunctionsPage extends InstancePage { } /** - * Is function displayed in the list - * @param sectionName The functions Name + * get all fields and all field's values from the section + * @param sectionName The section Name */ async getFieldsAndValuesBySection(sectionName: FunctionsSections): Promise { - return Selector(this.sectionMask.replace(/\$name/g, sectionName)).textContent; + return Selector(this.sectionMask.replace(/\$name/g, sectionName)).textContent; } } diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index 51b1b1cef6..fe2d6b85a2 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -46,7 +46,7 @@ test await browserPage.Cli.sendCommandInCli(command); await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); await t.click(triggersAndFunctionsFunctionsPage.librariesLink); - const row = await triggersAndFunctionsLibrariesPage.getLibraryItem(libraryName); + const row = await triggersAndFunctionsLibrariesPage.getLibraryItem(libraryName); await t.expect(row.name).eql(item.name, 'library name is unexpected'); await t.expect(row.user).eql(item.user, 'user name is unexpected'); await t.expect(row.pending).eql(item.pending, 'user name is unexpected'); From 2b8fde2180c7ab86554f8ed1f88e486eecab2fc3 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 30 Jun 2023 18:01:39 +0200 Subject: [PATCH 034/106] add tests for RI-4583 --- .../triggers-and-functions-libraries-page.ts | 21 ++- .../triggers-and-functions/libraries.e2e.ts | 157 +++++++++--------- 2 files changed, 96 insertions(+), 82 deletions(-) diff --git a/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts b/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts index 8e913ab77c..432b9d372c 100644 --- a/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts +++ b/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts @@ -11,6 +11,12 @@ export class TriggersAndFunctionsLibrariesPage extends InstancePage { textAreaMonaco = Selector('[class^=view-lines]'); configurationLink = Selector('[data-testid=library-view-tab-config]'); + functionsLink = Selector('[data-testid=triggered-functions-tab-functions]'); + + trashMask = '[data-testid=delete-library-icon-$name]'; + deleteMask = '[data-testid=delete-library-$name]'; + sectionMask = '[data-testid^=functions-$name]'; + functionMask = '[data-testid=func-$name]'; /** * Is library displayed in the table @@ -41,8 +47,8 @@ export class TriggersAndFunctionsLibrariesPage extends InstancePage { * @param functionsName The section Name */ getFunctionsByName(sectionName: LibrariesSections, functionsName: string): Selector { - const KeySpaceSection = Selector(`[data-testid^=functions-${sectionName}]`); - return KeySpaceSection.find(`[data-testid=func-${functionsName}]`); + const KeySpaceSection = Selector(this.sectionMask.replace(/\$name/g, sectionName)); + return KeySpaceSection.find(this.functionMask.replace(/\$name/g, functionsName)); } /** @@ -68,7 +74,16 @@ export class TriggersAndFunctionsLibrariesPage extends InstancePage { * Get text from monacoEditor */ async getTextToMonaco(): Promise { - return (await this.textAreaMonaco.textContent).replace(/\s+/g, ' '); } + + /** + * Delete library + * @param name The name os library + */ + async deleteLibraryByName(name: string){ + await t.hover(this.getLibraryNameSelector(name)) + .click(Selector(this.trashMask.replace(/\$name/g, name))) + .click(Selector(this.deleteMask.replace(/\$name/g, name))); + } } diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index fe2d6b85a2..b1df26a8bb 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -32,33 +32,26 @@ fixture `Triggers and Functions` }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); - }); - -test - .after(async() => { await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); - })('Verify that when user can see added library', async t => { - - const item = { name: libraryName, user: 'default', pending: 0, totalFunctions: 1 } as TriggersAndFunctionLibrary; - const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n redis.registerFunction(\'foo\', ()=>{return \'bar\'})"`; - await browserPage.Cli.sendCommandInCli(command); - await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); - await t.click(triggersAndFunctionsFunctionsPage.librariesLink); - const row = await triggersAndFunctionsLibrariesPage.getLibraryItem(libraryName); - await t.expect(row.name).eql(item.name, 'library name is unexpected'); - await t.expect(row.user).eql(item.user, 'user name is unexpected'); - await t.expect(row.pending).eql(item.pending, 'user name is unexpected'); - await t.expect(row.totalFunctions).eql(item.totalFunctions, 'user name is unexpected'); }); -test - .after(async() => { - await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); - await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); - })('Verify that library details is displayed', async t => { - const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n +test('Verify that when user can see added library', async t => { + + const item = { name: libraryName, user: 'default', pending: 0, totalFunctions: 1 } as TriggersAndFunctionLibrary; + const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n redis.registerFunction(\'foo\', ()=>{return \'bar\'})"`; + await browserPage.Cli.sendCommandInCli(command); + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + const row = await triggersAndFunctionsLibrariesPage.getLibraryItem(libraryName); + await t.expect(row.name).eql(item.name, 'library name is unexpected'); + await t.expect(row.user).eql(item.user, 'user name is unexpected'); + await t.expect(row.pending).eql(item.pending, 'user name is unexpected'); + await t.expect(row.totalFunctions).eql(item.totalFunctions, 'user name is unexpected'); +}); + +test('Verify that library details is displayed', async t => { + const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n redis.registerFunction('${LIBRARIES_LIST[0].name}', function(){}); redis.registerFunction('${LIBRARIES_LIST[1].name}', function(){}); redis.registerAsyncFunction('${LIBRARIES_LIST[2].name}', function(){}); @@ -66,62 +59,68 @@ test redis.registerClusterFunction('${LIBRARIES_LIST[4].name}', async function(){}); redis.registerKeySpaceTrigger('${LIBRARIES_LIST[5].name}','',function(){});"`; - await browserPage.Cli.sendCommandInCli(command); - - await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); - await t.click(triggersAndFunctionsFunctionsPage.librariesLink); - await t.click(await triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName)); - - for (const { name, type } of LIBRARIES_LIST) { - await t.expect(await triggersAndFunctionsLibrariesPage.getFunctionsByName(type, name).exists).ok(`library is not displayed in ${type} section`); - } - }); - -test - .after(async() => { - await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); - await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); - })('Verify that user can modify code', async t => { - const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n redis.registerFunction(\'foo\', ()=>{return \'bar\'});"`; - const commandUpdatedPart1 = `#!js api_version=1.0 name=${libraryName}`; - const commandUpdatedPart2 = ' redis.registerFunction(\'foo\', ()=>{return \'bar new\'});'; - const configuration = '{"redisgears_2.lock-redis-timeout": 1000}'; - - await browserPage.Cli.sendCommandInCli(command); - await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); - await t.click(triggersAndFunctionsFunctionsPage.librariesLink); - await t.click(await triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName)); - await t.click(triggersAndFunctionsLibrariesPage.editMonacoButton); - await triggersAndFunctionsLibrariesPage.sendTextToMonaco(commandUpdatedPart1, commandUpdatedPart2); - - await t.expect( - (await triggersAndFunctionsLibrariesPage.getTextToMonaco())).eql(commandUpdatedPart1 + commandUpdatedPart2), 'code was not updated'; - - await t.click(await triggersAndFunctionsLibrariesPage.configurationLink); - await t.click(triggersAndFunctionsLibrariesPage.editMonacoButton); - await triggersAndFunctionsLibrariesPage.sendTextToMonaco(configuration); - await t.expect( - (await triggersAndFunctionsLibrariesPage.getTextToMonaco())).eql(configuration, 'configuration was not added'); - }); - -test - .after(async() => { - await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); - await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); - })('Verify that function details is displayed', async t => { - const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n + await browserPage.Cli.sendCommandInCli(command); + + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + await t.click(await triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName)); + + for (const { name, type } of LIBRARIES_LIST) { + await t.expect(await triggersAndFunctionsLibrariesPage.getFunctionsByName(type, name).exists).ok(`library is not displayed in ${type} section`); + } +}); + +test('Verify that user can modify code', async t => { + const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n redis.registerFunction(\'foo\', ()=>{return \'bar\'});"`; + const commandUpdatedPart1 = `#!js api_version=1.0 name=${libraryName}`; + const commandUpdatedPart2 = ' redis.registerFunction(\'foo\', ()=>{return \'bar new\'});'; + const configuration = '{"redisgears_2.lock-redis-timeout": 1000}'; + + await browserPage.Cli.sendCommandInCli(command); + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + await t.click(await triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName)); + await t.click(triggersAndFunctionsLibrariesPage.editMonacoButton); + await triggersAndFunctionsLibrariesPage.sendTextToMonaco(commandUpdatedPart1, commandUpdatedPart2); + + await t.expect( + (await triggersAndFunctionsLibrariesPage.getTextToMonaco())).eql(commandUpdatedPart1 + commandUpdatedPart2), 'code was not updated'; + + await t.click(await triggersAndFunctionsLibrariesPage.configurationLink); + await t.click(triggersAndFunctionsLibrariesPage.editMonacoButton); + await triggersAndFunctionsLibrariesPage.sendTextToMonaco(configuration); + await t.expect( + (await triggersAndFunctionsLibrariesPage.getTextToMonaco())).eql(configuration, 'configuration was not added'); +}); + +test('Verify that function details is displayed', async t => { + const command = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n redis.registerAsyncFunction('${LIBRARIES_LIST[2].name}', function(client){ return client.call('ping');},{flags: [redis.functionFlags.RAW_ARGUMENTS]});"`; - const functionDetails = { libraryName: libraryName, isAsync: 'isAsync:Yes', flag: 'raw-arguments' }; - - await browserPage.Cli.sendCommandInCli(command); - await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); - await t.click(await triggersAndFunctionsFunctionsPage.getFunctionsNameSelector(LIBRARIES_LIST[2].name)); - let fieldsAndValue = await triggersAndFunctionsFunctionsPage.getFieldsAndValuesBySection(FunctionsSections.General); - await t.expect(fieldsAndValue).contains(functionDetails.libraryName, 'library name is not corrected'); - await t.expect(fieldsAndValue).contains(functionDetails.isAsync, 'async is not corrected'); - - fieldsAndValue = await triggersAndFunctionsFunctionsPage.getFieldsAndValuesBySection(FunctionsSections.Flag); - await t.expect(fieldsAndValue).contains(functionDetails.flag, 'flag name is not displayed'); - }); + const functionDetails = { libraryName: libraryName, isAsync: 'isAsync:Yes', flag: 'raw-arguments' }; + + await browserPage.Cli.sendCommandInCli(command); + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(await triggersAndFunctionsFunctionsPage.getFunctionsNameSelector(LIBRARIES_LIST[2].name)); + let fieldsAndValue = await triggersAndFunctionsFunctionsPage.getFieldsAndValuesBySection(FunctionsSections.General); + await t.expect(fieldsAndValue).contains(functionDetails.libraryName, 'library name is not corrected'); + await t.expect(fieldsAndValue).contains(functionDetails.isAsync, 'async is not corrected'); + + fieldsAndValue = await triggersAndFunctionsFunctionsPage.getFieldsAndValuesBySection(FunctionsSections.Flag); + await t.expect(fieldsAndValue).contains(functionDetails.flag, 'flag name is not displayed'); +}); +test('Verify that library and functions can be deleted', async t => { + + const libraryName2 = `${libraryName}2`; + const command1 = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName}\\n redis.registerFunction(\'${LIBRARIES_LIST[0].name}\', ()=>{return \'bar\'})"`; + const command2 = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName2}\\n redis.registerFunction(\'${LIBRARIES_LIST[1].name}\', ()=>{return \'bar\'})"`; + await browserPage.Cli.sendCommandInCli(command1); + await browserPage.Cli.sendCommandInCli(command2); + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + await triggersAndFunctionsLibrariesPage.deleteLibraryByName(libraryName2); + await t.expect(await triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName2).exists).notOk(`the library ${libraryName2} was not deleted`); + await t.click(triggersAndFunctionsLibrariesPage.functionsLink); + await t.expect(await triggersAndFunctionsFunctionsPage.getFunctionsNameSelector(LIBRARIES_LIST[1].name).exists).notOk(`the functions ${LIBRARIES_LIST[1].name} was not deleted`); +}); From ecc8781bd5664668be694eba56fb3d20e905510d Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Mon, 3 Jul 2023 12:28:14 +0300 Subject: [PATCH 035/106] Fe/feature/ri 4587 upload library (#2273) * #RI-4587 - add library upload form --- .../notifications/success-messages.tsx | 10 + .../src/components/upload-file/UploadFile.tsx | 9 +- .../triggeredFunctionsHandler.ts | 7 + .../pages/Libraries/LibrariesPage.spec.tsx | 59 +++++ .../pages/Libraries/LibrariesPage.tsx | 48 +++- .../components/AddLibrary/AddLibrary.spec.tsx | 114 +++++++++ .../components/AddLibrary/AddLibrary.tsx | 216 ++++++++++++++++++ .../Libraries/components/AddLibrary/index.ts | 3 + .../components/AddLibrary/styles.module.scss | 53 +++++ .../LibrariesList/LibrariesList.tsx | 4 +- .../NoLibrariesScreen.spec.tsx | 19 +- .../NoLibrariesScreen/NoLibrariesScreen.tsx | 22 +- .../NoLibrariesScreen/styles.module.scss | 3 + .../slices/interfaces/triggeredFunctions.ts | 3 + .../triggeredFunctions.spec.ts | 128 +++++++++++ .../triggeredFunctions/triggeredFunctions.ts | 58 +++++ redisinsight/ui/src/telemetry/events.ts | 4 + .../tests/triggered-functions/utils.spec.ts | 27 ++- .../ui/src/utils/triggered-functions/utils.ts | 13 ++ 19 files changed, 780 insertions(+), 20 deletions(-) create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/styles.module.scss diff --git a/redisinsight/ui/src/components/notifications/success-messages.tsx b/redisinsight/ui/src/components/notifications/success-messages.tsx index d9e56649ef..7fbd50291e 100644 --- a/redisinsight/ui/src/components/notifications/success-messages.tsx +++ b/redisinsight/ui/src/components/notifications/success-messages.tsx @@ -206,4 +206,14 @@ export default { ), }), + ADD_LIBRARY: (libraryName: string) => ({ + title: 'Library has been added', + message: ( + <> + {formatNameShort(libraryName)} + {' '} + has been added. + + ), + }) } diff --git a/redisinsight/ui/src/components/upload-file/UploadFile.tsx b/redisinsight/ui/src/components/upload-file/UploadFile.tsx index 211e0787e2..96c44f8467 100644 --- a/redisinsight/ui/src/components/upload-file/UploadFile.tsx +++ b/redisinsight/ui/src/components/upload-file/UploadFile.tsx @@ -7,20 +7,21 @@ export interface Props { onFileChange: (event: React.ChangeEvent) => void onClick?: () => void accept?: string + id?: string } -const UploadFile = ({ onFileChange, onClick, accept }: Props) => ( +const UploadFile = ({ onFileChange, onClick, accept, id = 'upload-input-file' }: Props) => ( onClick?.()} > -
@@ -196,7 +227,7 @@ const LibrariesPage = () => { paddingSize="none" wrapperProps={{ className: cx('triggeredFunctions__resizePanelRight', { - noVisible: !selectedRow + noVisible: !isRightPanelOpen }), }} > @@ -208,6 +239,9 @@ const LibrariesPage = () => { onDeleteRow={handleDelete} /> )} + {isAddLibraryPanelOpen && ( + + )}
diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.spec.tsx new file mode 100644 index 0000000000..fdabb900cc --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.spec.tsx @@ -0,0 +1,114 @@ +import React from 'react' +import { mock } from 'ts-mockito' +import userEvent from '@testing-library/user-event' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { render, screen, fireEvent, act, waitFor } from 'uiSrc/utils/test-utils' + +import AddLibrary, { IProps } from './AddLibrary' + +const mockedProps = mock() + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + instanceId: 'instanceId', + }), +})) + +describe('AddLibrary', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call proper telemetry event', async () => { + const onAdded = jest.fn() + + render( + + ) + fireEvent.change(screen.getByTestId('code-value'), { target: { value: 'code' } }) + + await act(() => { + fireEvent.click(screen.getByTestId('add-library-btn-submit')) + }) + + expect(onAdded).toBeCalled() + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LOADED, + eventData: { + databaseId: 'instanceId' + } + }) + }) + + it('should display configuration input', () => { + render() + + expect(screen.queryByTestId('configuration-value')).not.toBeInTheDocument() + + fireEvent.click(screen.getByTestId('show-configuration')) + + expect(screen.queryByTestId('configuration-value')).toBeInTheDocument() + }) + + it('should reset configuration input', () => { + render() + + fireEvent.click(screen.getByTestId('show-configuration')) + + fireEvent.change(screen.getByTestId('configuration-value'), { target: { value: 'config' } }) + + expect(screen.queryByTestId('configuration-value')).toHaveValue('config') + + fireEvent.click(screen.getByTestId('show-configuration')) + + expect(screen.queryByTestId('configuration-value')).not.toBeInTheDocument() + + fireEvent.click(screen.getByTestId('show-configuration')) + + expect(screen.queryByTestId('configuration-value')).toHaveValue('') + }) + + it('should upload js code file', async () => { + render( + + ) + + const file = new File(['123'], 'empty.js', { + type: 'application/javascript', + }) + const fileInput = screen.getByTestId('upload-code-file') + + expect(fileInput).toHaveAttribute('accept', '.js, text/plain') + + await userEvent.upload(fileInput, file) + + await waitFor(() => expect(screen.getByTestId('code-value')).toHaveValue('123')) + }) + + it('should upload json configuration file', async () => { + render( + + ) + + fireEvent.click(screen.getByTestId('show-configuration')) + + const jsonString = JSON.stringify({ a: 12 }) + const blob = new Blob([jsonString]) + const file = new File([blob], 'empty.json', { + type: 'application/JSON', + }) + const fileInput = screen.getByTestId('upload-configuration-file') + + expect(fileInput).toHaveAttribute('accept', 'application/json, text/plain') + + await userEvent.upload(fileInput, file) + + await waitFor(() => expect(screen.getByTestId('configuration-value')).toHaveValue('{"a":12}')) + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.tsx new file mode 100644 index 0000000000..64c8a66926 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.tsx @@ -0,0 +1,216 @@ +import React, { useState, ChangeEvent } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiButton, + EuiFormRow, + EuiTextColor, + EuiForm, + EuiCheckbox, + EuiText, + EuiToolTip, +} from '@elastic/eui' +import { + triggeredFunctionsAddLibrarySelector, + addTriggeredFunctionsLibraryAction, +} from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' + +import { MonacoJS, MonacoJson } from 'uiSrc/components/monaco-editor' +import UploadFile from 'uiSrc/components/upload-file' +import Divider from 'uiSrc/components/divider/Divider' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' + +import styles from './styles.module.scss' + +export interface IProps { + onClose: () => void + onAdded: (lib: string) => void +} + +const LibraryDetails = (props: IProps) => { + const { onClose, onAdded } = props + + const { loading } = useSelector(triggeredFunctionsAddLibrarySelector) + + const [code, setCode] = useState('') + const [configuration, setConfiguration] = useState('') + const [isShowConfiguration, setIsShowConfiguration] = useState(false) + + const { instanceId } = useParams<{ instanceId: string }>() + const dispatch = useDispatch() + + const onSuccess = (name: string) => { + onAdded(name) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_LOADED, + eventData: { + databaseId: instanceId, + } + }) + } + + const onFail = (error: string) => { + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARY_FAILED, + eventData: { + databaseId: instanceId, + error, + } + }) + } + + const onSubmit = () => { + if (code) { + dispatch(addTriggeredFunctionsLibraryAction( + instanceId, + code, + configuration, + onSuccess, + onFail, + )) + } + } + + const handleChangeConfigurationCheckbox = (e: ChangeEvent): void => { + const isChecked = e.target.checked + if (!isChecked) { + // Reset configuration field to initial value + setConfiguration('') + } + setIsShowConfiguration(isChecked) + } + + const onFileChange = ( + { target: { files } }: { target: { files: FileList | null } }, + callback: (value: string) => void + ) => { + if (files && files[0]) { + const reader = new FileReader() + reader.onload = async (e) => { + callback(e?.target?.result as string) + } + reader.readAsText(files[0]) + } + } + + return ( +
+
+ Load New library + + + +
+
+ + + Library Code + + )} + fullWidth + > + <> + + + + onFileChange(e, setCode)} accept=".js, text/plain" /> + + + + + + + + {isShowConfiguration && ( + <> + + + Library Configuration + + )} + className={styles.configurationSection} + fullWidth + > + <> + + + + onFileChange(e, setConfiguration)} accept="application/json, text/plain" /> + + + + + + )} + +
+
+ + Cancel + + + + Add Library + + +
+
+ ) +} + +export default LibraryDetails diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/index.ts new file mode 100644 index 0000000000..bab242beec --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/index.ts @@ -0,0 +1,3 @@ +import AddLibrary from './AddLibrary' + +export default AddLibrary diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/styles.module.scss new file mode 100644 index 0000000000..f6dc9512e6 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/styles.module.scss @@ -0,0 +1,53 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/global_styling/index"; + +.main { + position: relative; + height: 100%; + + .header { + height: 58px; + padding: 16px; + border-bottom: 1px solid var(--euiColorLightShade); + + .titleTooltip { + width: auto; + max-width: calc(100% - 80px); + } + } + + .content { + position: relative; + padding: 16px 30px 40px; + + @include euiScrollBar; + overflow: auto; + height: 100%; + max-height: calc(100% - 104px); + margin-bottom: 46px; + } + + .label { + font-size: 14px; + margin-bottom: 12px; + } + + .divider { + margin: 26px 10px; + } + + .footer { + position: absolute; + bottom: 0; + display: flex; + height: 46px; + justify-content: flex-end; + align-items: center; + width: 100%; + padding: 16px; + } + + .cancelBtn { + margin-right: 10px + } +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx index 7cc9db1c8d..0e0e0552c4 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx @@ -31,7 +31,7 @@ const NoLibrariesMessage: React.ReactNode = (
@@ -16,7 +20,19 @@ const NoLibrariesScreen = () => ( See an overview of triggers and functions uploaded, upload new libraries, and manage the list of existing ones. - To start working with triggers and functions, click “+ Library” to upload a new library. + To start working with triggers and functions, click + + + Library + + to upload a new library. +
) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss index c1c56b3e68..256e0c9068 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss @@ -25,3 +25,6 @@ color: transparent; } +.btn { + margin: 0 6px; +} diff --git a/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts b/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts index 762cf2fe10..330b2e6b3e 100644 --- a/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts +++ b/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts @@ -66,4 +66,7 @@ export interface StateTriggeredFunctions { data: Nullable loading: boolean } + addLibrary: { + loading: boolean + } } diff --git a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts index 81757210a0..5a792029cc 100644 --- a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts +++ b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts @@ -26,6 +26,10 @@ import reducer, { deleteTriggeredFunctionsLibrarySuccess, deleteTriggeredFunctionsLibraryFailure, deleteTriggeredFunctionsLibraryAction, + addTriggeredFunctionsLibrary, + addTriggeredFunctionsLibrarySuccess, + addTriggeredFunctionsLibraryFailure, + addTriggeredFunctionsLibraryAction, } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { apiService } from 'uiSrc/services' import { addMessageNotification, addErrorNotification } from 'uiSrc/slices/app/notifications' @@ -494,6 +498,81 @@ describe('triggeredFunctions slice', () => { expect(triggeredFunctionsSelector(rootState)).toEqual(state) }) }) + + describe('addTriggeredFunctionsLibrary', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + addLibrary: { + loading: true + } + } + + // Act + const nextState = reducer(initialState, addTriggeredFunctionsLibrary()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('addTriggeredFunctionsLibrarySuccess', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + addLibrary: { + loading: true, + }, + } + const state = { + ...initialState, + addLibrary: { + loading: false, + }, + } + + // Act + const nextState = reducer(currentState, addTriggeredFunctionsLibrarySuccess()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) + + describe('addTriggeredFunctionsLibraryFailure', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + addLibrary: { + loading: true, + }, + } + const state = { + ...initialState, + addLibrary: { + loading: false, + }, + } + + // Act + const nextState = reducer(currentState, addTriggeredFunctionsLibraryFailure()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) }) // thunks @@ -692,6 +771,55 @@ describe('triggeredFunctions slice', () => { }) }) + describe('addTriggeredFunctionsLibraryAction', () => { + it('succeed to fetch data', async () => { + const responsePayload = { status: 200 } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch( + addTriggeredFunctionsLibraryAction('123', 'code', 'config') + ) + + // Assert + const expectedActions = [ + addTriggeredFunctionsLibrary(), + addTriggeredFunctionsLibrarySuccess(), + addMessageNotification(successMessages.ADD_LIBRARY('Library')), + getTriggeredFunctionsLibrariesList(), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch data', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch( + addTriggeredFunctionsLibraryAction('123', 'code', 'config') + ) + + // Assert + const expectedActions = [ + addTriggeredFunctionsLibrary(), + addErrorNotification(responsePayload as AxiosError), + addTriggeredFunctionsLibraryFailure() + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) + describe('deleteTriggeredFunctionsLibraryAction', () => { it('succeed to delete libraries', async () => { const responsePayload = { status: 200 } diff --git a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts index 5d8f8e2db0..f0baf40712 100644 --- a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts +++ b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts @@ -11,6 +11,7 @@ import { import { AppDispatch, RootState } from 'uiSrc/slices/store' import { apiService } from 'uiSrc/services' import { getApiErrorMessage, getUrl, isStatusSuccessful, Nullable } from 'uiSrc/utils' +import { getLibraryName } from 'uiSrc/utils/triggered-functions/utils' import { ApiEndpoints } from 'uiSrc/constants' import { addMessageNotification, addErrorNotification } from 'uiSrc/slices/app/notifications' @@ -35,6 +36,9 @@ export const initialState: StateTriggeredFunctions = { loading: false, data: null }, + addLibrary: { + loading: false, + }, } const triggeredFunctionsSlice = createSlice({ @@ -110,6 +114,16 @@ const triggeredFunctionsSlice = createSlice({ setSelectedLibraryToShow: (state, { payload }: PayloadAction>) => { state.libraries.selected = payload }, + + addTriggeredFunctionsLibrary: (state) => { + state.addLibrary.loading = true + }, + addTriggeredFunctionsLibrarySuccess: (state) => { + state.addLibrary.loading = false + }, + addTriggeredFunctionsLibraryFailure: (state) => { + state.addLibrary.loading = false + }, } }) @@ -133,12 +147,16 @@ export const { deleteTriggeredFunctionsLibraryFailure, setSelectedFunctionToShow, setSelectedLibraryToShow, + addTriggeredFunctionsLibrary, + addTriggeredFunctionsLibrarySuccess, + addTriggeredFunctionsLibraryFailure, } = triggeredFunctionsSlice.actions export const triggeredFunctionsSelector = (state: RootState) => state.triggeredFunctions export const triggeredFunctionsLibrariesSelector = (state: RootState) => state.triggeredFunctions.libraries export const triggeredFunctionsFunctionsSelector = (state: RootState) => state.triggeredFunctions.functions export const triggeredFunctionsSelectedLibrarySelector = (state: RootState) => state.triggeredFunctions.selectedLibrary +export const triggeredFunctionsAddLibrarySelector = (state: RootState) => state.triggeredFunctions.addLibrary export default triggeredFunctionsSlice.reducer @@ -271,6 +289,46 @@ export function replaceTriggeredFunctionsLibraryAction( } } +export function addTriggeredFunctionsLibraryAction( + instanceId: string, + code: string, + configuration: Nullable, + onSuccessAction?: (name: string) => void, + onFailAction?: (error: string) => void, +) { + return async (dispatch: AppDispatch) => { + try { + dispatch(addTriggeredFunctionsLibrary()) + + const { status } = await apiService.post( + getUrl( + instanceId, + ApiEndpoints.TRIGGERED_FUNCTIONS_LIBRARY, + ), + { + code, + configuration + } + ) + + if (isStatusSuccessful(status)) { + const libraryName = getLibraryName(code) + dispatch(addTriggeredFunctionsLibrarySuccess()) + dispatch( + addMessageNotification(successMessages.ADD_LIBRARY(libraryName)) + ) + dispatch(fetchTriggeredFunctionsLibrariesList(instanceId)) + onSuccessAction?.(libraryName) + } + } catch (_err) { + const error = _err as AxiosError + dispatch(addErrorNotification(error)) + dispatch(addTriggeredFunctionsLibraryFailure()) + onFailAction?.(getApiErrorMessage(error)) + } + } +} + export function deleteTriggeredFunctionsLibraryAction( instanceId: string, libraryName: string, diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 3e498c9471..ac2089c00c 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -260,4 +260,8 @@ export enum TelemetryEvent { TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_AUTO_REFRESH_DISABLED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_LIST_AUTO_REFRESH_DISABLED', TRIGGERS_AND_FUNCTIONS_LIBRARIES_RECEIVED = 'TRIGGERS_AND_FUNCTIONS_LIBRARIES_RECEIVED', TRIGGERS_AND_FUNCTIONS_FUNCTIONS_RECEIVED = 'TRIGGERS_AND_FUNCTIONS_FUNCTIONS_RECEIVED', + TRIGGERS_AND_FUNCTIONS_LOAD_LIBRARY_CLICKED = 'TRIGGERS_AND_FUNCTIONS_LOAD_LIBRARY_CLICKED', + TRIGGERS_AND_FUNCTIONS_LOAD_LIBRARY_CANCELLED = 'TRIGGERS_AND_FUNCTIONS_LOAD_LIBRARY_CANCELLED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_LOADED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_LOADED', + TRIGGERS_AND_FUNCTIONS_LIBRARY_FAILED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_FAILED', } diff --git a/redisinsight/ui/src/utils/tests/triggered-functions/utils.spec.ts b/redisinsight/ui/src/utils/tests/triggered-functions/utils.spec.ts index 159cf5117a..fa5ce539a0 100644 --- a/redisinsight/ui/src/utils/tests/triggered-functions/utils.spec.ts +++ b/redisinsight/ui/src/utils/tests/triggered-functions/utils.spec.ts @@ -1,4 +1,4 @@ -import { getFunctionsLengthByType } from 'uiSrc/utils/triggered-functions' +import { getFunctionsLengthByType, getLibraryName } from 'uiSrc/utils/triggered-functions' import { TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA } from 'uiSrc/mocks/data/triggeredFunctions' import { FunctionType } from 'uiSrc/slices/interfaces/triggeredFunctions' @@ -26,3 +26,28 @@ describe('getFunctionsLengthByType', () => { }) }) }) + +const getLibraryNamesTests: any[] = [ + [`#!js api_version=1.0 name=lib + redis.registerStreamTrigger( + 'consumer', 'stream', async function(c, data) + { redis.log(JSON.stringify(data, (key, value) => + typeof value === 'bigint' ? value.toString() : value )); + }`, 'lib'], + ['!js api_version=1.0 name=lib1', 'lib1'], + ['!js name=lib1 api_version=1.0', 'lib1'], + ['!js name=lib1_21@% api_version=1.0', 'lib1_21@%'], + [1, 'Library'], + [' ', 'Library'], + [null, 'Library'], + ['#!js api_version=1.0', 'Library'], + ['#!js api_version=1.0 name = name', 'Library'], +] + +describe('getLibraryName', () => { + it.each(getLibraryNamesTests)('for input: %s (reply), should be output: %s', + (reply, expected) => { + const result = getLibraryName(reply) + expect(result).toBe(expected) + }) +}) diff --git a/redisinsight/ui/src/utils/triggered-functions/utils.ts b/redisinsight/ui/src/utils/triggered-functions/utils.ts index 1f4b732b1f..caf1733f48 100644 --- a/redisinsight/ui/src/utils/triggered-functions/utils.ts +++ b/redisinsight/ui/src/utils/triggered-functions/utils.ts @@ -8,3 +8,16 @@ export const getFunctionsLengthByType = (functions: Array<{ ...current, [next.type]: functions?.filter((f) => f.type === next.type).length || 0 }), {}) + +const DEFAULT_LIBRARY_NAME = 'Library' + +export const getLibraryName = (code: string): string => { + try { + const firstLine = code.split('\n')[0] + const regexp = /name=[^\s\\]+/ + const matches = firstLine.match(regexp) + return matches ? matches[0].split('=')[1].trim() : DEFAULT_LIBRARY_NAME + } catch (err) { + return DEFAULT_LIBRARY_NAME + } +} From b89c1e66ddef187fdc8e471b77668974b6186962 Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Tue, 4 Jul 2023 12:16:04 +0300 Subject: [PATCH 036/106] #RI-4698 - reset input value (#2275) * #RI-4698 - reset input value --- .../upload-file/UploadFile.spec.tsx | 44 +++++++++++++- .../src/components/upload-file/UploadFile.tsx | 59 ++++++++++++------- .../AddKeyReJSON/AddKeyReJSON.spec.tsx | 3 +- .../add-key/AddKeyReJSON/AddKeyReJSON.tsx | 12 +--- .../components/AddLibrary/AddLibrary.tsx | 17 +----- 5 files changed, 85 insertions(+), 50 deletions(-) diff --git a/redisinsight/ui/src/components/upload-file/UploadFile.spec.tsx b/redisinsight/ui/src/components/upload-file/UploadFile.spec.tsx index f0aba82805..c65e608f9d 100644 --- a/redisinsight/ui/src/components/upload-file/UploadFile.spec.tsx +++ b/redisinsight/ui/src/components/upload-file/UploadFile.spec.tsx @@ -1,6 +1,6 @@ import React from 'react' import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' +import { render, fireEvent, screen, waitFor } from 'uiSrc/utils/test-utils' import UploadFile, { Props } from './UploadFile' @@ -12,4 +12,46 @@ describe('UploadFile', () => { render() ).toBeTruthy() }) + + it('should call onClick', () => { + const onClick = jest.fn() + + render() + + fireEvent.click(screen.getByTestId('upload-file-btn')) + + expect(onClick).toHaveBeenCalledTimes(1) + }) + + it('should read file', async () => { + const onFileChange = jest.fn() + + const jsonString = JSON.stringify({ a: 12 }) + const blob = new Blob([jsonString]) + const file = new File([blob], 'empty.json', { + type: 'application/JSON', + }) + render() + + const fileInput = screen.getByTestId('upload-input-file') + fireEvent.change( + fileInput, + { target: { files: [file] } } + ) + await waitFor(() => expect(onFileChange).toBeCalled()) + await waitFor(() => expect(screen.getByTestId('upload-input-file')).toHaveValue('')) + }) + + it('should not call onFileChange', async () => { + const onFileChange = jest.fn() + + render() + + const fileInput = screen.getByTestId('upload-input-file') + fireEvent.change( + fileInput, + { target: { files: [] } } + ) + await waitFor(() => expect(onFileChange).not.toBeCalled()) + }) }) diff --git a/redisinsight/ui/src/components/upload-file/UploadFile.tsx b/redisinsight/ui/src/components/upload-file/UploadFile.tsx index 96c44f8467..030e5b2f85 100644 --- a/redisinsight/ui/src/components/upload-file/UploadFile.tsx +++ b/redisinsight/ui/src/components/upload-file/UploadFile.tsx @@ -4,31 +4,48 @@ import { EuiButtonEmpty, EuiText, EuiIcon } from '@elastic/eui' import styles from './styles.module.scss' export interface Props { - onFileChange: (event: React.ChangeEvent) => void + onFileChange: (string: string) => void onClick?: () => void accept?: string id?: string } -const UploadFile = ({ onFileChange, onClick, accept, id = 'upload-input-file' }: Props) => ( - onClick?.()} - > - - -) +const UploadFile = (props: Props) => { + const { onFileChange, onClick, accept, id = 'upload-input-file' } = props + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + const reader = new FileReader() + reader.onload = async (e) => { + onFileChange(e?.target?.result as string) + } + reader.readAsText(e.target.files[0]) + // reset input value after reading file + e.target.value = '' + } + } + + return ( + onClick?.()} + data-testid="upload-file-btn" + > + + + ) +} export default UploadFile diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.spec.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.spec.tsx index c8baa14b56..026a3edbc0 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.spec.tsx @@ -96,11 +96,10 @@ describe('AddKeyReJSON', () => { const fileInput = screen.getByTestId('upload-input-file') expect(fileInput).toHaveAttribute('accept', 'application/json, text/plain') - expect(fileInput.files.length).toBe(0) await userEvent.upload(fileInput, file) - expect(fileInput.files.length).toBe(1) + await waitFor(() => expect(screen.getByTestId('json-value')).toHaveValue('{"a":12}')) }) it('should set the value from json file', async () => { diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.tsx index fb958f0733..aa9dab83a4 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.tsx @@ -72,16 +72,6 @@ const AddKeyReJSON = (props: Props) => { dispatch(addReJSONKey(data, onCancel)) } - const onFileChange = ({ target: { files } }: { target: { files: FileList | null } }) => { - if (files && files[0]) { - const reader = new FileReader() - reader.onload = async (e) => { - setReJSONValue(e?.target?.result as string) - } - reader.readAsText(files[0]) - } - } - const onClick = () => { sendEventTelemetry({ event: TelemetryEvent.BROWSER_JSON_VALUE_IMPORT_CLICKED, @@ -103,7 +93,7 @@ const AddKeyReJSON = (props: Props) => { /> - + diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.tsx index 64c8a66926..9ea01ccbd0 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/AddLibrary/AddLibrary.tsx @@ -85,19 +85,6 @@ const LibraryDetails = (props: IProps) => { setIsShowConfiguration(isChecked) } - const onFileChange = ( - { target: { files } }: { target: { files: FileList | null } }, - callback: (value: string) => void - ) => { - if (files && files[0]) { - const reader = new FileReader() - reader.onload = async (e) => { - callback(e?.target?.result as string) - } - reader.readAsText(files[0]) - } - } - return (
@@ -136,7 +123,7 @@ const LibraryDetails = (props: IProps) => { /> - onFileChange(e, setCode)} accept=".js, text/plain" /> + @@ -172,7 +159,7 @@ const LibraryDetails = (props: IProps) => { /> - onFileChange(e, setConfiguration)} accept="application/json, text/plain" /> + From 0258320379d8814826b3ac3d6a32c40c826ac0ce Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 4 Jul 2023 14:34:15 +0300 Subject: [PATCH 037/106] improve .dockerignore --- .dockerignore | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/.dockerignore b/.dockerignore index 8d16a29403..d70e64f513 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,18 +4,15 @@ .circleci .docker -coverage -dll **/node_modules release -redisinsight/dist -redisinsight/node_modules -redisinsight/main.prod.js - -redisinsight/api/.nyc_output -redisinsight/api/coverage -redisinsight/api/dist -redisinsight/api/node_modules +**/dist +**/coverage +**/dll +**/.issues +**/.parcel-cache +**/.temp_cache +**/.nyc_output -redisinsight/ui/dist +redisinsight/main.prod.js From 0ca3ebd71ae57009e7a50bc9e9ef65842af304a9 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 4 Jul 2023 16:40:15 +0200 Subject: [PATCH 038/106] test run with skipped --- .../tests/critical-path/database/modules.e2e.ts | 6 +++--- tests/e2e/tsconfig.json | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/tsconfig.json diff --git a/tests/e2e/tests/critical-path/database/modules.e2e.ts b/tests/e2e/tests/critical-path/database/modules.e2e.ts index 5e113adbc2..d5b2fb865a 100644 --- a/tests/e2e/tests/critical-path/database/modules.e2e.ts +++ b/tests/e2e/tests/critical-path/database/modules.e2e.ts @@ -24,7 +24,7 @@ fixture `Database modules` // Delete database await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); -test +test.skip .meta({ rte: rte.standalone, env: env.web })('Verify that user can see DB modules on DB list page for Standalone DB', async t => { // Check module column on DB list page await t.expect(myRedisDatabasePage.moduleColumn.exists).ok('Module column not found'); @@ -48,7 +48,7 @@ test //Verify that user can hover over the module icons and see tooltip with version. await myRedisDatabasePage.checkModulesInTooltip(moduleNameList); }); -test +test.skip .meta({ rte: rte.standalone })('Verify that user can see full module list in the Edit mode', async t => { // Verify that module column is displayed await t.expect(myRedisDatabasePage.moduleColumn.visible).ok('Module column not found'); @@ -59,7 +59,7 @@ test // Verify modules in Edit mode await myRedisDatabasePage.checkModulesOnPage(moduleList); }); -test +test.skip .meta({ rte: rte.standalone })('Verify that user can see icons in DB header for RediSearch, RedisGraph, RedisJSON, RedisBloom, RedisTimeSeries, RedisGears, RedisAI default modules', async t => { // Connect to DB await myRedisDatabasePage.clickOnDBByName(ossStandaloneRedisearch.databaseName); diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json new file mode 100644 index 0000000000..4e920cf41d --- /dev/null +++ b/tests/e2e/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "sourceMap": true + }, + "exclude": [ + "./node_modules", + "**/node_modules" + ] +} From 3a768738506cdffcc3320f7f79a2c794fd41fc49 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 5 Jul 2023 12:39:02 +0200 Subject: [PATCH 039/106] #RI-4594 - Invoke function --- .../add-items-actions/AddItemsActions.tsx | 24 +-- .../FunctionDetails/FunctionDetails.spec.tsx | 43 +++++ .../FunctionDetails/FunctionDetails.tsx | 65 ++++++- .../FunctionDetails/styles.module.scss | 8 +- .../InvokeFunction/InvokeFunction.spec.tsx | 117 +++++++++++++ .../InvokeFunction/InvokeFunction.tsx | 162 ++++++++++++++++++ .../InvokeFunction/InvokeFunctionForm.tsx | 149 ++++++++++++++++ .../components/InvokeFunction/index.ts | 3 + .../InvokeFunction/styles.module.scss | 15 ++ .../pages/Libraries/LibrariesPage.tsx | 39 +++-- redisinsight/ui/src/telemetry/events.ts | 3 + redisinsight/ui/src/utils/commands.ts | 21 ++- .../ui/src/utils/tests/commands.spec.ts | 35 +++- 13 files changed, 649 insertions(+), 35 deletions(-) create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunction.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunction.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunctionForm.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/add-items-actions/AddItemsActions.tsx b/redisinsight/ui/src/pages/browser/components/add-items-actions/AddItemsActions.tsx index 1a2b881656..48c5955d42 100644 --- a/redisinsight/ui/src/pages/browser/components/add-items-actions/AddItemsActions.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-items-actions/AddItemsActions.tsx @@ -3,15 +3,16 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/e interface Props { id: number - length: number, - index: number, - loading: boolean, - removeItem: (id: number) => void, - addItem: () => void, - anchorClassName: string, - clearItemValues?: (id: number) => void, - clearIsDisabled?: boolean, - addItemIsDisabled?: boolean, + length: number + index: number + loading: boolean + removeItem: (id: number) => void + addItem: () => void + anchorClassName: string + clearItemValues?: (id: number) => void + clearIsDisabled?: boolean + addItemIsDisabled?: boolean + 'data-testid'?: string } const AddItemsActions = (props: Props) => { @@ -25,7 +26,8 @@ const AddItemsActions = (props: Props) => { anchorClassName, clearItemValues, clearIsDisabled, - addItemIsDisabled + addItemIsDisabled, + 'data-testid': dataTestId } = props const handleClick = () => { @@ -79,7 +81,7 @@ const AddItemsActions = (props: Props) => { disabled={loading || addItemIsDisabled} aria-label="Add new item" onClick={addItem} - data-testid="add-new-item" + data-testid={dataTestId || 'add-new-item'} />
diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx index e196782e4e..cd880e2b17 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx @@ -136,4 +136,47 @@ describe('FunctionDetails', () => { expect(pushMock).toHaveBeenCalledTimes(1) expect(pushMock).toHaveBeenCalledWith('/instanceId/triggered-functions/libraries') }) + + it('should render invoke button', () => { + render() + + expect(screen.getByTestId('invoke-btn')).toBeInTheDocument() + }) + + it('should not render invoke button', () => { + render() + + expect(screen.queryByTestId('invoke-btn')).not.toBeInTheDocument() + }) + + it('should call proper telemetry events on invoke', async () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render() + + sendEventTelemetry.mockRestore() + + fireEvent.click(screen.getByTestId('invoke-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CLICKED, + eventData: { + databaseId: 'instanceId', + isAsync: mockedItem.isAsync + } + }) + + sendEventTelemetry.mockRestore() + + fireEvent.click(screen.getByTestId('cancel-invoke-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CANCELLED, + eventData: { + databaseId: 'instanceId', + } + }) + }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx index 9a782b468b..64b03c78c0 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx @@ -1,14 +1,24 @@ -import React, { useEffect } from 'react' -import { EuiBadge, EuiButtonIcon, EuiCollapsibleNavGroup, EuiLink, EuiText, EuiTitle, EuiToolTip, } from '@elastic/eui' +import React, { useEffect, useState } from 'react' +import { + EuiBadge, + EuiButton, + EuiButtonIcon, + EuiCollapsibleNavGroup, + EuiLink, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui' import cx from 'classnames' import { isNil } from 'lodash' import { useHistory, useParams } from 'react-router-dom' import { useDispatch } from 'react-redux' -import { TriggeredFunctionsFunction } from 'uiSrc/slices/interfaces/triggeredFunctions' +import { FunctionType, TriggeredFunctionsFunction } from 'uiSrc/slices/interfaces/triggeredFunctions' import { Pages } from 'uiSrc/constants' import { setSelectedLibraryToShow } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import InvokeFunction from 'uiSrc/pages/triggeredFunctions/pages/Functions/components/InvokeFunction' import styles from './styles.module.scss' export interface Props { @@ -18,7 +28,9 @@ export interface Props { const FunctionDetails = (props: Props) => { const { item, onClose } = props - const { name, library, description, flags, lastError, totalExecutionTime, lastExecutionTime } = item + const { name, library, type, description, flags, lastError, totalExecutionTime, lastExecutionTime } = item + + const [isInvokeOpen, setIsInvokeOpen] = useState(false) const { instanceId } = useParams<{ instanceId: string }>() const history = useHistory() @@ -35,6 +47,27 @@ const FunctionDetails = (props: Props) => { }) }, [item]) + const handleClickInvoke = () => { + setIsInvokeOpen(true) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CLICKED, + eventData: { + databaseId: instanceId, + isAsync: item?.isAsync + } + }) + } + + const handleCancelInvoke = () => { + setIsInvokeOpen(false) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CANCELLED, + eventData: { + databaseId: instanceId, + } + }) + } + const goToLibrary = (e: React.MouseEvent, libName: string) => { e.preventDefault() dispatch(setSelectedLibraryToShow(libName)) @@ -48,6 +81,8 @@ const FunctionDetails = (props: Props) => {
) + const isShowInvokeButton = type === FunctionType.Function + return (
@@ -58,6 +93,18 @@ const FunctionDetails = (props: Props) => { > {name} + {isShowInvokeButton && ( + + Invoke + + )} { )}
+ {isInvokeOpen && ( +
+ +
+ )}
) } diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/styles.module.scss index 873d1561fd..66dd5f9918 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/styles.module.scss +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/styles.module.scss @@ -7,13 +7,17 @@ height: 100%; .header { + display: flex; + align-items: center; + justify-content: space-between; height: 58px; - padding: 16px; + padding: 16px 48px 16px 16px; border-bottom: 1px solid var(--euiColorLightShade); + .titleTooltip { width: auto; - max-width: calc(100% - 80px); + max-width: calc(100% - 120px); } } diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunction.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunction.spec.tsx new file mode 100644 index 0000000000..d68995b5e8 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunction.spec.tsx @@ -0,0 +1,117 @@ +import React from 'react' +import { mock } from 'ts-mockito' +import { cloneDeep } from 'lodash' +import { render, screen, act, fireEvent, mockedStore, cleanup, clearStoreActions } from 'uiSrc/utils/test-utils' + +import { openCli } from 'uiSrc/slices/cli/cli-settings' +import { concatToOutput, sendCliCommand, updateCliCommandHistory } from 'uiSrc/slices/cli/cli-output' +import { cliCommandOutput } from 'uiSrc/utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import InvokeFunction, { Props } from './InvokeFunction' + +jest.mock('uiSrc/slices/cli/cli-settings', () => ({ + ...jest.requireActual('uiSrc/slices/cli/cli-settings'), + cliSettingsSelector: jest.fn().mockReturnValue({ + cliClientUuid: '123' + }) +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +const mockedProps = mock() + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('InvokeFunction', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should properly render form and preview on init', async () => { + await act(() => { + render() + }) + + expect(screen.getByTestId('keyname-field-0')).toBeInTheDocument() + expect(screen.getByTestId('argument-field-0')).toBeInTheDocument() + expect(screen.getByTestId('redis-command-preview')).toBeInTheDocument() + }) + + it('should properly change form, change preview and call proper actions on submit', async () => { + const expectedCommand = 'TFCALL "lib.foo" "1" "keyName" "argument-1" "argument-2"' + await act(() => { + render() + }) + + fireEvent.change( + screen.getByTestId('keyname-field-0'), + { target: { value: 'keyName' } } + ) + + fireEvent.change( + screen.getByTestId('argument-field-0'), + { target: { value: 'argument-1' } } + ) + + fireEvent.click(screen.getByTestId('add-new-argument-item')) + + fireEvent.change( + screen.getByTestId('argument-field-1'), + { target: { value: 'argument-2' } } + ) + + expect(screen.getByTestId('redis-command-preview')) + .toHaveTextContent(expectedCommand) + + fireEvent.click(screen.getByTestId('invoke-function-btn')) + + const expectedActions = [ + openCli(), + concatToOutput(cliCommandOutput(expectedCommand, 0)), + updateCliCommandHistory([expectedCommand]), + sendCliCommand() + ] + + expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions([...expectedActions])) + }) + + it('should call onCancel', async () => { + const onCancel = jest.fn() + await act(() => { + render() + }) + + fireEvent.click(screen.getByTestId('cancel-invoke-btn')) + + expect(onCancel).toBeCalled() + }) + + it('should call proper telemetry events', async () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + await act(() => { + render() + }) + + fireEvent.click(screen.getByTestId('invoke-function-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_REQUESTED, + eventData: { + databaseId: 'instanceId', + } + }) + + sendEventTelemetry.mockRestore() + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunction.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunction.tsx new file mode 100644 index 0000000000..d8c9bf25cb --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunction.tsx @@ -0,0 +1,162 @@ +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import cx from 'classnames' +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiPanel, EuiTextColor } from '@elastic/eui' +import { SendClusterCommandDto } from 'src/modules/cli/dto/cli.dto' +import { useParams } from 'react-router-dom' +import { + concatToOutput, + outputSelector, + sendCliClusterCommandAction, + sendCliCommandAction +} from 'uiSrc/slices/cli/cli-output' +import { cliSettingsSelector, openCli } from 'uiSrc/slices/cli/cli-settings' + +import { generateRedisCommand } from 'uiSrc/utils/commands' +import { CodeBlock } from 'uiSrc/components' +import { cliCommandOutput, updateCliHistoryStorage } from 'uiSrc/utils' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { ConnectionType } from 'uiSrc/slices/interfaces' +import { ClusterNodeRole } from 'uiSrc/slices/interfaces/cli' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import InvokeFunctionForm from './InvokeFunctionForm' + +import styles from './styles.module.scss' + +export interface Props { + libName: string + name: string + isAsync?: boolean + onCancel: () => void +} + +const InvokeFunction = (props: Props) => { + const { libName, name, isAsync, onCancel } = props + + const { cliClientUuid } = useSelector(cliSettingsSelector) + const { db: currentDbIndex } = useSelector(outputSelector) + const { host, port, connectionType } = useSelector(connectedInstanceSelector) + + const [keyNames, setKeyNames] = useState>([]) + const [args, setArgs] = useState>([]) + const [redisCommand, setRedisCommand] = useState('') + const [isSubmitted, setIsSubmitted] = useState(false) + + const { instanceId } = useParams<{ instanceId: string }>() + const dispatch = useDispatch() + + useEffect(() => { + if (isSubmitted) { + dispatch(openCli()) + } + + // wait for cli connected + if (cliClientUuid && isSubmitted) { + sendCommand() + } + }, [cliClientUuid, isSubmitted, redisCommand]) + + useEffect(() => { + setRedisCommand( + generateRedisCommand( + isAsync ? 'TFCALLASYNC' : 'TFCALL', + `${libName}.${name}`, + [keyNames.length, keyNames], + args + ) + ) + }, [keyNames, args, libName, name]) + + const handleSubmit = () => { + setIsSubmitted(true) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_REQUESTED, + eventData: { + databaseId: instanceId, + } + }) + } + + const sendCommand = () => { + dispatch(concatToOutput(cliCommandOutput(redisCommand, currentDbIndex))) + updateCliHistoryStorage(redisCommand, dispatch) + setIsSubmitted(false) + + if (connectionType !== ConnectionType.Cluster) { + dispatch(sendCliCommandAction(redisCommand)) + return + } + + const options: SendClusterCommandDto = { + command: redisCommand, + nodeOptions: { + host, + port, + enableRedirection: true, + }, + role: ClusterNodeRole.All, + } + dispatch(sendCliClusterCommandAction(redisCommand, options)) + } + + return ( + <> + + + + + {redisCommand} + + + + + + + onCancel()} + className="btn-cancel btn-back" + data-testid="cancel-invoke-btn" + > + Cancel + + + + + Run in CLI + + + + + + ) +} + +export default InvokeFunction diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunctionForm.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunctionForm.tsx new file mode 100644 index 0000000000..0e19337526 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/InvokeFunctionForm.tsx @@ -0,0 +1,149 @@ +import React, { ChangeEvent, useEffect, useRef, useState } from 'react' +import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui' +import { map } from 'lodash' +import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' + +import styles from './styles.module.scss' + +export interface Props { + name: string + libName: string + onChangeKeys: (items: string[]) => void + onChangeArgs: (items: string[]) => void +} + +const initialFieldValue = [{ id: 0, value: '' }] + +const InvokeFunctionForm = (props: Props) => { + const { name, libName, onChangeArgs, onChangeKeys } = props + + const [keyNames, setKeyNames] = useState(initialFieldValue) + const [args, setArgs] = useState(initialFieldValue) + + const isInitialLoad = useRef(true) + const lastAddedKeyName = useRef(null) + const lastAddedArgument = useRef(null) + + useEffect(() => { + setKeyNames(initialFieldValue) + setArgs(initialFieldValue) + }, [libName, name]) + + useEffect(() => lastAddedKeyName.current?.focus(), [keyNames.length]) + useEffect(() => { + if (!isInitialLoad.current) { + lastAddedArgument.current?.focus() + } + isInitialLoad.current = false + }, [args.length]) + + useEffect(() => onChangeKeys(map(keyNames, 'value').filter((name) => name)), [keyNames]) + useEffect(() => onChangeArgs(map(args, 'value')), [args]) + + const handleAddKeyName = () => { + const lastField = keyNames[keyNames.length - 1] + setKeyNames((prev) => ([...prev, { id: lastField.id + 1, value: '' }])) + } + + const handleKeyNameChange = (id: number, value: string) => + setKeyNames((items) => items.map((item) => { + if (item.id === id) return { ...item, value } + return item + })) + + const handleRemoveKeyName = (id: number) => + setKeyNames((items) => items.filter((item) => item.id !== id)) + + const handleAddArgument = () => { + const lastField = args[args.length - 1] + setArgs((prev) => ([...prev, { id: lastField.id + 1, value: '' }])) + } + + const handleArgumentChange = (id: number, value: string) => + setArgs((items) => items.map((item) => { + if (item.id === id) return { ...item, value } + return item + })) + + const handleRemoveArgument = (id: number) => + setArgs((items) => items.filter((item) => item.id !== id)) + + return ( + <> + + + {keyNames.map(({ id, value }, index) => ( + + + + + ) => handleKeyNameChange(id, e.target.value)} + inputRef={index === keyNames.length - 1 ? lastAddedKeyName : null} + data-testid={`keyname-field-${id}`} + autoComplete="off" + /> + + + + + + ))} + + + + + {args.map(({ id, value }, index) => ( + + + + + ) => handleArgumentChange(id, e.target.value)} + inputRef={index === args.length - 1 ? lastAddedArgument : null} + data-testid={`argument-field-${id}`} + autoComplete="off" + /> + + + + + + ))} + + + + ) +} + +export default InvokeFunctionForm diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/index.ts new file mode 100644 index 0000000000..dc4c8497ba --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/index.ts @@ -0,0 +1,3 @@ +import InvokeFunction from './InvokeFunction' + +export default InvokeFunction diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/styles.module.scss new file mode 100644 index 0000000000..aa8800f92e --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeFunction/styles.module.scss @@ -0,0 +1,15 @@ +.content { + border: none !important; + border-top: 1px solid var(--euiColorPrimary) !important; + max-height: 400px; +} + +.preview { + background-color: var(--euiColorEmptyShade); + color: var(--euiColorMediumShade); + font: normal normal normal 13px/17px Inconsolata !important; + + min-height: 50px; + word-wrap: break-word; + white-space: break-spaces; +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx index ded7862da9..16610398af 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx @@ -49,26 +49,33 @@ const LibrariesPage = () => { applyFiltering() }, [filterValue, libraries]) - const updateList = () => { - dispatch(fetchTriggeredFunctionsLibrariesList(instanceId, (librariesList) => { - if (selected) { - const findRow = find(librariesList, (item) => item.name === selected) + const handleSuccessUpdateList = (data: TriggeredFunctionsLibrary[]) => { + if (selectedRow) { + const findRow = find(data, (item) => item.name === selectedRow) + setSelectedRow(findRow?.name ?? null) + } + + if (selected) { + const findRow = find(data, (item) => item.name === selected) - if (findRow) { - setSelectedRow(selected) - } + if (findRow) { + setSelectedRow(selected) + } + + dispatch(setSelectedLibraryToShow(null)) + } - dispatch(setSelectedLibraryToShow(null)) + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARIES_RECEIVED, + eventData: { + databaseId: instanceId, + total: data.length } + }) + } - sendEventTelemetry({ - event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARIES_RECEIVED, - eventData: { - databaseId: instanceId, - total: librariesList.length - } - }) - })) + const updateList = () => { + dispatch(fetchTriggeredFunctionsLibrariesList(instanceId, handleSuccessUpdateList)) } const onChangeFiltering = (e: React.ChangeEvent) => { diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index ac2089c00c..34ab199677 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -264,4 +264,7 @@ export enum TelemetryEvent { TRIGGERS_AND_FUNCTIONS_LOAD_LIBRARY_CANCELLED = 'TRIGGERS_AND_FUNCTIONS_LOAD_LIBRARY_CANCELLED', TRIGGERS_AND_FUNCTIONS_LIBRARY_LOADED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_LOADED', TRIGGERS_AND_FUNCTIONS_LIBRARY_FAILED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_FAILED', + TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_REQUESTED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_REQUESTED', + TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CLICKED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CLICKED', + TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CANCELLED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CANCELLED' } diff --git a/redisinsight/ui/src/utils/commands.ts b/redisinsight/ui/src/utils/commands.ts index 035610df50..932e14b24d 100644 --- a/redisinsight/ui/src/utils/commands.ts +++ b/redisinsight/ui/src/utils/commands.ts @@ -1,4 +1,4 @@ -import { flatten, isArray, isEmpty, isNumber, reject, toNumber, isNaN, isInteger } from 'lodash' +import { flatten, isArray, isEmpty, isNumber, reject, toNumber, isNaN, isInteger, isString, forEach } from 'lodash' import { CommandArgsType, CommandProvider, @@ -206,3 +206,22 @@ export const getCommandRepeat = (command = ''): [string, number] => { } export const isRepeatCountCorrect = (number: number): boolean => number >= 1 && isInteger(number) + +type RedisArg = string | number | Array +export const generateRedisCommand = ( + command: string, + ...rest: Array +) => { + let commandToSend = command + + forEach(rest, (arg: RedisArg) => { + if ((isString(arg) && arg) || isNumber(arg)) { + commandToSend += ` "${arg}"` + } + if (isArray(arg)) { + commandToSend += generateRedisCommand('', ...arg) + } + }) + + return commandToSend.replace(/\s\s+/g, ' ') +} diff --git a/redisinsight/ui/src/utils/tests/commands.spec.ts b/redisinsight/ui/src/utils/tests/commands.spec.ts index 9c3121b5be..0cd374a0dd 100644 --- a/redisinsight/ui/src/utils/tests/commands.spec.ts +++ b/redisinsight/ui/src/utils/tests/commands.spec.ts @@ -1,5 +1,11 @@ import { ICommandArgGenerated, ICommands, MOCK_COMMANDS_SPEC } from 'uiSrc/constants' -import { generateArgs, generateArgsNames, getComplexityShortNotation, getDocUrlForCommand } from '../commands' +import { + generateArgs, + generateArgsNames, + getComplexityShortNotation, + getDocUrlForCommand, + generateRedisCommand, +} from '../commands' import { cleanup } from '../test-utils' const ALL_REDIS_COMMANDS: ICommands = MOCK_COMMANDS_SPEC @@ -191,3 +197,30 @@ describe('getDocUrlForCommand', () => { expect(result).toBe(expected) }) }) + +const generateRedisCommandTests = [ + { + input: ['info'], + output: 'info' + }, + { + input: ['set', ['a', 'b']], + output: 'set "a" "b"' + }, + { + input: ['set', 'a', 'b'], + output: 'set "a" "b"' + }, + { + input: ['command', ['a', 'b'], ['b', 'b'], 0, 'a', 'a b c'], + output: 'command "a" "b" "b" "b" "0" "a" "a b c"' + }, +] + +describe('generateRedisCommand', () => { + it.each(generateRedisCommandTests)('for input: %s (input), should be output: %s', + ({ input, output }) => { + const result = generateRedisCommand(...input) + expect(result).toBe(output) + }) +}) From d4d2419f108b64be95860f6d34892e04f66328b2 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Thu, 6 Jul 2023 09:30:12 +0200 Subject: [PATCH 040/106] add test for file uploading --- .../components/monaco-editor/MonacoEditor.tsx | 3 +- .../common-actions/common-elements-actions.ts | 17 +++++++ tests/e2e/helpers/constants.ts | 11 ++++- .../triggers-and-functions-libraries-page.ts | 29 +++++++---- .../triggers-and-functions/library.txt | 15 ++++++ .../triggers-and-functions/libraries.e2e.ts | 48 ++++++++++++++----- 6 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 tests/e2e/common-actions/common-elements-actions.ts create mode 100644 tests/e2e/test-data/triggers-and-functions/library.txt diff --git a/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx index 22c91f4522..a330c6d1b8 100644 --- a/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx +++ b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx @@ -112,7 +112,7 @@ const MonacoEditor = (props: Props) => { declineOnUnmount={false} preventOutsideClick > -
+
{ options={monacoOptions} className={cx(styles.editor, className, { readMode: !isEditing && readOnly })} editorDidMount={editorDidMount} - data-testid={dataTestId} />
diff --git a/tests/e2e/common-actions/common-elements-actions.ts b/tests/e2e/common-actions/common-elements-actions.ts new file mode 100644 index 0000000000..7bab00a94d --- /dev/null +++ b/tests/e2e/common-actions/common-elements-actions.ts @@ -0,0 +1,17 @@ +import { t } from 'testcafe'; + +export class CommonElementsActions { + + /** + * Select Checkbox + * @param checkbox Selector of the checkbox to check + * @param value value of the checkbox + */ + static async checkCheckbox(checkbox: Selector, value: boolean): Promise { + + if (await checkbox.checked !== value) { + await t.click(checkbox); + } + } + +} diff --git a/tests/e2e/helpers/constants.ts b/tests/e2e/helpers/constants.ts index 3fbd120364..0429b60eee 100644 --- a/tests/e2e/helpers/constants.ts +++ b/tests/e2e/helpers/constants.ts @@ -57,5 +57,14 @@ export enum LibrariesSections { export enum FunctionsSections { General = 'General', - Flag = 'Flag' + Flag = 'Flag', +} + +export enum MonacoEditorInputs { + //add library fields + Code = 'code-value', + Configuration = 'configuration-value', + // added library fields + Library = 'library-code', + LibraryConfiguration = 'library-configuration', } diff --git a/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts b/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts index 432b9d372c..d6642103df 100644 --- a/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts +++ b/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts @@ -1,22 +1,35 @@ import { Selector, t } from 'testcafe'; import { TriggersAndFunctionLibrary } from '../interfaces/triggers-and-functions'; -import { LibrariesSections } from '../helpers/constants'; +import { LibrariesSections, MonacoEditorInputs } from '../helpers/constants'; import { InstancePage } from './instance-page'; export class TriggersAndFunctionsLibrariesPage extends InstancePage { + //Buttons editMonacoButton = Selector('[data-testid=edit-monaco-value]'); acceptButton = Selector('[data-testid=apply-btn]'); + addLibraryButton = Selector('[data-testid=btn-add-library]'); + uploadFileButton = Selector('[data-testid=upload-file-btn]'); + addLibrarySubmitButton = Selector('[data-testid=add-library-btn-submit]'); - inputMonaco = Selector('[class=inlineMonacoEditor]'); + //CheckBoxes + addConfigurationCheckBox = Selector('[data-testid=show-configuration] ~ label'); + + //Inputs textAreaMonaco = Selector('[class^=view-lines]'); + //Links configurationLink = Selector('[data-testid=library-view-tab-config]'); functionsLink = Selector('[data-testid=triggered-functions-tab-functions]'); + // Import + uploadInput = Selector('[data-testid=upload-code-file]', { timeout: 2000 }); + + //Masks trashMask = '[data-testid=delete-library-icon-$name]'; deleteMask = '[data-testid=delete-library-$name]'; sectionMask = '[data-testid^=functions-$name]'; functionMask = '[data-testid=func-$name]'; + inputMonaco = '[data-testid=$name]'; /** * Is library displayed in the table @@ -56,24 +69,24 @@ export class TriggersAndFunctionsLibrariesPage extends InstancePage { * @param commandPart1 The command that should be on the first line * @param commandPart2 command part except mandatory part */ - async sendTextToMonaco(commandPart1: string, commandPart2?: string): Promise { + async sendTextToMonaco(input: MonacoEditorInputs, commandPart1: string, commandPart2?: string): Promise { + const inputSelector = Selector(this.inputMonaco.replace(/\$name/g, input)); await t // remove text since replace doesn't work here .pressKey('ctrl+a') .pressKey('delete') - .typeText(this.inputMonaco, commandPart1); + .typeText(inputSelector, commandPart1); if (commandPart2) { await t.pressKey('enter') - .typeText(this.inputMonaco, commandPart2); + .typeText(inputSelector, commandPart2); } - await t.click(this.acceptButton); } /** * Get text from monacoEditor */ - async getTextToMonaco(): Promise { + async getTextFromMonaco(): Promise { return (await this.textAreaMonaco.textContent).replace(/\s+/g, ' '); } @@ -81,7 +94,7 @@ export class TriggersAndFunctionsLibrariesPage extends InstancePage { * Delete library * @param name The name os library */ - async deleteLibraryByName(name: string){ + async deleteLibraryByName(name: string): Promise { await t.hover(this.getLibraryNameSelector(name)) .click(Selector(this.trashMask.replace(/\$name/g, name))) .click(Selector(this.deleteMask.replace(/\$name/g, name))); diff --git a/tests/e2e/test-data/triggers-and-functions/library.txt b/tests/e2e/test-data/triggers-and-functions/library.txt new file mode 100644 index 0000000000..f2693c9120 --- /dev/null +++ b/tests/e2e/test-data/triggers-and-functions/library.txt @@ -0,0 +1,15 @@ +#!js api_version=1.0 name=lib +var last_update_field_name = '__last_update__' + +if (redis.config.last_update_field_name !== undefined) { + if (typeof redis.config.last_update_field_name != 'string') { + throw "last_update_field_name must be a string"; + } + last_update_field_name = redis.config.last_update_field_name +} + +redis.registerFunction("function", function(client, key, field, val){ + // get the current time in ms + var curr_time = client.call("time")[0]; + return client.call('hset', key, field, val, last_update_field_name, curr_time); +}); diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index b1df26a8bb..98b97c5b6a 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -1,13 +1,15 @@ +import * as path from 'path'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { BrowserPage, TriggersAndFunctionsLibrariesPage } from '../../../pageObjects'; import { - commonUrl, - ossStandaloneRedisGears -} from '../../../helpers/conf'; -import { rte, LibrariesSections, FunctionsSections } from '../../../helpers/constants'; + BrowserPage, + TriggersAndFunctionsFunctionsPage, + TriggersAndFunctionsLibrariesPage +} from '../../../pageObjects'; +import { commonUrl, ossStandaloneRedisGears } from '../../../helpers/conf'; +import { FunctionsSections, LibrariesSections, MonacoEditorInputs, rte } from '../../../helpers/constants'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { TriggersAndFunctionLibrary } from '../../../interfaces/triggers-and-functions'; -import { TriggersAndFunctionsFunctionsPage } from '../../../pageObjects/triggers-and-functions-functions-page'; +import { CommonElementsActions } from '../../../common-actions/common-elements-actions'; const browserPage = new BrowserPage(); const triggersAndFunctionsLibrariesPage = new TriggersAndFunctionsLibrariesPage(); @@ -15,6 +17,9 @@ const triggersAndFunctionsFunctionsPage = new TriggersAndFunctionsFunctionsPage( const libraryName = 'lib'; +//const filesToUpload = ['bulkUplAllKeyTypes.txt', 'bigKeysData.rtf']; +const filePath = path.join('..', '..', '..', 'test-data', 'triggers-and-functions', 'library.txt'); + const LIBRARIES_LIST = [ { name: 'Function1', type: LibrariesSections.Functions }, { name: 'function2', type: LibrariesSections.Functions }, @@ -81,16 +86,17 @@ test('Verify that user can modify code', async t => { await t.click(triggersAndFunctionsFunctionsPage.librariesLink); await t.click(await triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName)); await t.click(triggersAndFunctionsLibrariesPage.editMonacoButton); - await triggersAndFunctionsLibrariesPage.sendTextToMonaco(commandUpdatedPart1, commandUpdatedPart2); - + await triggersAndFunctionsLibrariesPage.sendTextToMonaco(MonacoEditorInputs.Library, commandUpdatedPart1, commandUpdatedPart2); + await t.click(triggersAndFunctionsLibrariesPage.acceptButton); await t.expect( - (await triggersAndFunctionsLibrariesPage.getTextToMonaco())).eql(commandUpdatedPart1 + commandUpdatedPart2), 'code was not updated'; + (await triggersAndFunctionsLibrariesPage.getTextFromMonaco())).eql(commandUpdatedPart1 + commandUpdatedPart2), 'code was not updated'; await t.click(await triggersAndFunctionsLibrariesPage.configurationLink); await t.click(triggersAndFunctionsLibrariesPage.editMonacoButton); - await triggersAndFunctionsLibrariesPage.sendTextToMonaco(configuration); + await triggersAndFunctionsLibrariesPage.sendTextToMonaco(MonacoEditorInputs.LibraryConfiguration, configuration); + await t.click(triggersAndFunctionsLibrariesPage.acceptButton); await t.expect( - (await triggersAndFunctionsLibrariesPage.getTextToMonaco())).eql(configuration, 'configuration was not added'); + (await triggersAndFunctionsLibrariesPage.getTextFromMonaco())).eql(configuration, 'configuration was not added'); }); test('Verify that function details is displayed', async t => { @@ -116,11 +122,27 @@ test('Verify that library and functions can be deleted', async t => { const command2 = `TFUNCTION LOAD "#!js api_version=1.0 name=${libraryName2}\\n redis.registerFunction(\'${LIBRARIES_LIST[1].name}\', ()=>{return \'bar\'})"`; await browserPage.Cli.sendCommandInCli(command1); await browserPage.Cli.sendCommandInCli(command2); - await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); - await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + await t.click(await browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(await triggersAndFunctionsFunctionsPage.librariesLink); await triggersAndFunctionsLibrariesPage.deleteLibraryByName(libraryName2); await t.expect(await triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName2).exists).notOk(`the library ${libraryName2} was not deleted`); await t.click(triggersAndFunctionsLibrariesPage.functionsLink); await t.expect(await triggersAndFunctionsFunctionsPage.getFunctionsNameSelector(LIBRARIES_LIST[1].name).exists).notOk(`the functions ${LIBRARIES_LIST[1].name} was not deleted`); }); +test.only('Verify that library can be uploaded', async t => { + const configuration = '{"redisgears_2.lock-redis-timeout": 1000}'; + const functionNameFromFile = 'function'; + + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + await t.click(triggersAndFunctionsLibrariesPage.addLibraryButton); + await t.setFilesToUpload(triggersAndFunctionsLibrariesPage.uploadInput, [filePath]); + const uploadedText = await triggersAndFunctionsLibrariesPage.getTextFromMonaco(); + await t.expect(uploadedText.length).gte(1, 'file was not uploaded'); + await CommonElementsActions.checkCheckbox(triggersAndFunctionsLibrariesPage.addConfigurationCheckBox, true); + await triggersAndFunctionsLibrariesPage.sendTextToMonaco(MonacoEditorInputs.Configuration, configuration); + await t.click(await triggersAndFunctionsLibrariesPage.addLibrarySubmitButton); + await t.expect(triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName).exists).ok('the library was not added'); + await t.expect(triggersAndFunctionsLibrariesPage.getFunctionsByName(LibrariesSections.Functions, functionNameFromFile).exists).ok('the library information was not opened'); +}); From 8c6630a0a65113fafaa794d75da96a2daf4377ed Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Thu, 6 Jul 2023 09:33:47 +0200 Subject: [PATCH 041/106] remove only --- .../tests/critical-path/triggers-and-functions/libraries.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index 98b97c5b6a..eb1b0ae1ac 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -129,7 +129,7 @@ test('Verify that library and functions can be deleted', async t => { await t.click(triggersAndFunctionsLibrariesPage.functionsLink); await t.expect(await triggersAndFunctionsFunctionsPage.getFunctionsNameSelector(LIBRARIES_LIST[1].name).exists).notOk(`the functions ${LIBRARIES_LIST[1].name} was not deleted`); }); -test.only('Verify that library can be uploaded', async t => { +test('Verify that library can be uploaded', async t => { const configuration = '{"redisgears_2.lock-redis-timeout": 1000}'; const functionNameFromFile = 'function'; From b8a800fe727a0970a122ba5ab128b20f4bd1d22d Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Thu, 6 Jul 2023 10:04:17 +0200 Subject: [PATCH 042/106] comment fix --- .../tests/critical-path/triggers-and-functions/libraries.e2e.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index eb1b0ae1ac..c7e1573c19 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -16,8 +16,6 @@ const triggersAndFunctionsLibrariesPage = new TriggersAndFunctionsLibrariesPage( const triggersAndFunctionsFunctionsPage = new TriggersAndFunctionsFunctionsPage(); const libraryName = 'lib'; - -//const filesToUpload = ['bulkUplAllKeyTypes.txt', 'bigKeysData.rtf']; const filePath = path.join('..', '..', '..', 'test-data', 'triggers-and-functions', 'library.txt'); const LIBRARIES_LIST = [ From 52118a95085efcbce59438bad4e26b13faa3a5ef Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 6 Jul 2023 12:41:24 +0300 Subject: [PATCH 043/106] upgrade linux executor machine --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 83059e1d11..a3d0263454 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -173,10 +173,10 @@ orbs: executors: linux-executor: machine: - image: ubuntu-2004:202010-01 + image: ubuntu-2004:2023.04.2 linux-executor-dlc: machine: - image: ubuntu-2004:202010-01 + image: ubuntu-2004:2023.04.2 docker_layer_caching: true jobs: From 7028811de572806bf18deec85564bd49527f0c60 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 6 Jul 2023 12:51:10 +0300 Subject: [PATCH 044/106] fix docker network name --- redisinsight/api/test/test-runs/start-test-run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/test/test-runs/start-test-run.sh b/redisinsight/api/test/test-runs/start-test-run.sh index 77e5e567c5..a50dd28e26 100755 --- a/redisinsight/api/test/test-runs/start-test-run.sh +++ b/redisinsight/api/test/test-runs/start-test-run.sh @@ -33,7 +33,7 @@ then fi # Unique ID for the test run -ID=$RTE-$(tr -dc A-Za-z0-9 Date: Thu, 6 Jul 2023 15:46:40 +0200 Subject: [PATCH 045/106] #RI-4595 - add invoke stream functions --- .../ui/src/pages/browser/BrowserPage.spec.tsx | 1 + .../ui/src/pages/browser/BrowserPage.tsx | 8 + .../stream-details/stream-tabs/StreamTabs.tsx | 21 +-- .../FunctionDetails/FunctionDetails.spec.tsx | 13 +- .../FunctionDetails/FunctionDetails.tsx | 34 ++-- .../InvokeStreamTrigger.spec.tsx | 102 +++++++++++ .../InvokeStreamTrigger.tsx | 166 ++++++++++++++++++ .../components/InvokeStreamTrigger/index.ts | 3 + .../InvokeStreamTrigger/styles.module.scss | 9 + redisinsight/ui/src/slices/browser/keys.ts | 9 +- redisinsight/ui/src/telemetry/events.ts | 3 +- 11 files changed, 340 insertions(+), 29 deletions(-) create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/InvokeStreamTrigger.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/InvokeStreamTrigger.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/index.ts create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx b/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx index d4ce4dbf77..33f914415d 100644 --- a/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx +++ b/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx @@ -162,6 +162,7 @@ describe('BrowserPage', () => { setBrowserBulkActionOpen(expect.any(Boolean)), setBrowserSelectedKey(null), setLastPageContext('browser'), + toggleBrowserFullScreen(false) ] expect(store.getActions()).toEqual([...afterRenderActions, ...unmountActions]) diff --git a/redisinsight/ui/src/pages/browser/BrowserPage.tsx b/redisinsight/ui/src/pages/browser/BrowserPage.tsx index ccd599c2dd..b63cc97f5e 100644 --- a/redisinsight/ui/src/pages/browser/BrowserPage.tsx +++ b/redisinsight/ui/src/pages/browser/BrowserPage.tsx @@ -99,6 +99,10 @@ const BrowserPage = () => { dispatch(setBrowserBulkActionOpen(isBulkActionsPanelOpenRef.current)) dispatch(setBrowserSelectedKey(selectedKeyRef.current)) dispatch(setLastPageContext('browser')) + + if (!selectedKeyRef.current) { + dispatch(toggleBrowserFullScreen(false)) + } } }, []) @@ -106,6 +110,10 @@ const BrowserPage = () => { isBulkActionsPanelOpenRef.current = isBulkActionsPanelOpen }, [isBulkActionsPanelOpen]) + useEffect(() => { + setSelectedKey(selectedKeyContext) + }, [selectedKeyContext]) + useEffect(() => { selectedKeyRef.current = selectedKey }, [selectedKey]) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx index 85e61aa914..faef5280fc 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx @@ -79,18 +79,15 @@ const StreamTabs = () => { }) } - return tabs.map(({ id, label, separator = '' }) => ( - <> - {separator} - onSelectedTabChanged(id)} - key={id} - data-testid={`stream-tab-${id}`} - > - {label} - - + return tabs.map(({ id, label }) => ( + onSelectedTabChanged(id)} + key={id} + data-testid={`stream-tab-${id}`} + > + {label} + )) }, [viewType, selectedGroupName, selectedConsumerName]) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx index cd880e2b17..7056efe757 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.spec.tsx @@ -33,6 +33,7 @@ beforeEach(() => { }) const mockedItem = TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA[0] +const mockedItemStreamTrigger = TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA[4] const mockedItemKeySpaceTriggers = TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA[3] describe('FunctionDetails', () => { @@ -137,12 +138,18 @@ describe('FunctionDetails', () => { expect(pushMock).toHaveBeenCalledWith('/instanceId/triggered-functions/libraries') }) - it('should render invoke button', () => { + it('should render invoke button with function type', () => { render() expect(screen.getByTestId('invoke-btn')).toBeInTheDocument() }) + it('should render invoke button with stream triggers type', () => { + render() + + expect(screen.getByTestId('invoke-btn')).toBeInTheDocument() + }) + it('should not render invoke button', () => { render() @@ -164,7 +171,8 @@ describe('FunctionDetails', () => { event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CLICKED, eventData: { databaseId: 'instanceId', - isAsync: mockedItem.isAsync + isAsync: mockedItem.isAsync, + functionType: 'functions', } }) @@ -176,6 +184,7 @@ describe('FunctionDetails', () => { event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CANCELLED, eventData: { databaseId: 'instanceId', + functionType: 'functions', } }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx index 64b03c78c0..845cc89d8a 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionDetails/FunctionDetails.tsx @@ -19,6 +19,7 @@ import { Pages } from 'uiSrc/constants' import { setSelectedLibraryToShow } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import InvokeFunction from 'uiSrc/pages/triggeredFunctions/pages/Functions/components/InvokeFunction' +import InvokeStreamTrigger from 'uiSrc/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger' import styles from './styles.module.scss' export interface Props { @@ -53,7 +54,8 @@ const FunctionDetails = (props: Props) => { event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CLICKED, eventData: { databaseId: instanceId, - isAsync: item?.isAsync + isAsync: item?.isAsync, + functionType: item.type, } }) } @@ -64,6 +66,7 @@ const FunctionDetails = (props: Props) => { event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CANCELLED, eventData: { databaseId: instanceId, + functionType: item.type, } }) } @@ -81,7 +84,7 @@ const FunctionDetails = (props: Props) => {
) - const isShowInvokeButton = type === FunctionType.Function + const isShowInvokeButton = type === FunctionType.Function || type === FunctionType.StreamTrigger return (
@@ -185,14 +188,25 @@ const FunctionDetails = (props: Props) => { )}
{isInvokeOpen && ( -
- -
+ <> + {type === FunctionType.Function && ( +
+ +
+ )} + {type === FunctionType.StreamTrigger && ( +
+ +
+ )} + )}
) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/InvokeStreamTrigger.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/InvokeStreamTrigger.spec.tsx new file mode 100644 index 0000000000..2b633744f5 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/InvokeStreamTrigger.spec.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import { cloneDeep } from 'lodash' +import reactRouterDom from 'react-router-dom' +import { cleanup, clearStoreActions, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' + +import { + changeSearchMode, + loadKeys, + resetKeyInfo, + resetKeysData, + setFilter, + setSearchMatch +} from 'uiSrc/slices/browser/keys' +import { SearchMode } from 'uiSrc/slices/interfaces/keys' +import { resetBrowserTree, setBrowserSelectedKey, setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' +import { DEFAULT_DELIMITER, KeyTypes } from 'uiSrc/constants' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import InvokeStreamTrigger from './InvokeStreamTrigger' + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: jest.fn, + }), +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('InvokeStreamTrigger', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call onCancel', () => { + const onCancel = jest.fn() + + render() + + fireEvent.click(screen.getByTestId('cancel-invoke-btn')) + + expect(onCancel).toBeCalled() + }) + + it('should call proper actions on submit', () => { + render() + + fireEvent.change( + screen.getByTestId('keyName-field'), + { target: { value: 'key*' } } + ) + fireEvent.click(screen.getByTestId('find-key-btn')) + + const expectedActions = [ + changeSearchMode(SearchMode.Pattern), + setBrowserTreeDelimiter(DEFAULT_DELIMITER), + setFilter(KeyTypes.Stream), + setSearchMatch('key*', SearchMode.Pattern), + resetKeysData(SearchMode.Pattern), + resetBrowserTree(), + resetKeyInfo(), + setBrowserSelectedKey(null), + loadKeys(), + ] + + expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions([...expectedActions])) + }) + + it('should call push history on submit', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + render() + + fireEvent.click(screen.getByTestId('find-key-btn')) + expect(pushMock).toHaveBeenCalledWith('/instanceId/browser') + }) + + it('should call proper telemetry on submit', () => { + const sendEventTelemetryMock = jest.fn() + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render() + + fireEvent.click(screen.getByTestId('find-key-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FIND_KEY_CLICKED, + eventData: { + databaseId: 'instanceId', + } + }) + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/InvokeStreamTrigger.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/InvokeStreamTrigger.tsx new file mode 100644 index 0000000000..77e49f5b7f --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/InvokeStreamTrigger.tsx @@ -0,0 +1,166 @@ +import React, { ChangeEvent, useState } from 'react' +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiIcon, + EuiPanel, + EuiTextColor, + EuiToolTip +} from '@elastic/eui' +import cx from 'classnames' + +import { useDispatch, useSelector } from 'react-redux' +import { useHistory, useParams } from 'react-router-dom' +import { GetKeysWithDetailsResponse } from 'src/modules/browser/dto' +import { + changeSearchMode, + fetchKeys, + keysSelector, + resetKeyInfo, + resetKeysData, + setFilter, + setSearchMatch +} from 'uiSrc/slices/browser/keys' +import { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys' +import { + appContextDbConfig, + resetBrowserTree, + setBrowserKeyListDataLoaded, + setBrowserSelectedKey, + setBrowserTreeDelimiter +} from 'uiSrc/slices/app/context' +import { SCAN_COUNT_DEFAULT, SCAN_TREE_COUNT_DEFAULT } from 'uiSrc/constants/api' +import { KeyTypes, Pages } from 'uiSrc/constants' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import styles from './styles.module.scss' + +export interface Props { + onCancel: () => void +} + +const InvokeStreamTrigger = ({ onCancel }: Props) => { + const { treeViewDelimiter: delimiter = '' } = useSelector(appContextDbConfig) + const { viewType } = useSelector(keysSelector) + + const [keyName, setKeyName] = useState('') + + const { instanceId } = useParams<{ instanceId: string }>() + const history = useHistory() + const dispatch = useDispatch() + + const handleSubmit = () => { + dispatch(changeSearchMode(SearchMode.Pattern)) + dispatch(setBrowserTreeDelimiter(delimiter)) + dispatch(setFilter(KeyTypes.Stream)) + dispatch(setSearchMatch(keyName, SearchMode.Pattern)) + dispatch(resetKeysData(SearchMode.Pattern)) + dispatch(resetBrowserTree()) + dispatch(resetKeyInfo()) + dispatch(setBrowserSelectedKey(null)) + + dispatch(fetchKeys( + { + searchMode: SearchMode.Pattern, + cursor: '0', + count: viewType === KeyViewType.Browser ? SCAN_COUNT_DEFAULT : SCAN_TREE_COUNT_DEFAULT, + }, + (data) => { + const keys = data as GetKeysWithDetailsResponse[] + dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, true)) + + if (keys[0].keys.length === 1) { + dispatch(setBrowserSelectedKey(keys[0].keys[0].name as RedisResponseBuffer)) + } + }, + () => dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, false)), + )) + + history.push(Pages.browser(instanceId)) + + sendEventTelemetry({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FIND_KEY_CLICKED, + eventData: { + databaseId: instanceId + } + }) + } + + const label = ( + <> + Key Name + + + + + ) + + return ( + + + + ) => setKeyName(e.target.value)} + data-testid="keyName-field" + autoComplete="off" + /> + + + + + + onCancel()} + className="btn-cancel btn-back" + data-testid="cancel-invoke-btn" + > + Cancel + + + + + Find in Browser + + + + + + ) +} + +export default InvokeStreamTrigger diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/index.ts new file mode 100644 index 0000000000..f7bec02121 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/index.ts @@ -0,0 +1,3 @@ +import InvokeStreamTrigger from './InvokeStreamTrigger' + +export default InvokeStreamTrigger diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/styles.module.scss new file mode 100644 index 0000000000..5c93993080 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/InvokeStreamTrigger/styles.module.scss @@ -0,0 +1,9 @@ +.content { + border: none !important; + border-top: 1px solid var(--euiColorPrimary) !important; + max-height: 400px; +} + +.tooltipAnchor { + margin-left: 4px; +} diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index baaaede52e..7bb8ea69b9 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -38,6 +38,7 @@ import { CreateRejsonRlWithExpireDto, CreateSetWithExpireDto, GetKeyInfoResponse, + GetKeysWithDetailsResponse, } from 'apiSrc/modules/browser/dto' import { CreateStreamDto } from 'apiSrc/modules/browser/dto/stream.dto' @@ -499,7 +500,7 @@ export function fetchPatternKeysAction( cursor: string, count: number, telemetryProperties: { [key: string]: any } = {}, - onSuccess?: () => void, + onSuccess?: (data: GetKeysWithDetailsResponse[]) => void, onFailed?: () => void ) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { @@ -515,7 +516,7 @@ export function fetchPatternKeysAction( const { search: match, filter: type } = state.browser.keys const { encoding } = state.app.info - const { data, status } = await apiService.post( + const { data, status } = await apiService.post( getUrl( state.connections.instances?.connectedInstance?.id ?? '', ApiEndpoints.KEYS @@ -576,7 +577,7 @@ export function fetchPatternKeysAction( } }) } - onSuccess?.() + onSuccess?.(data) } } catch (error) { if (!axios.isCancel(error)) { @@ -1158,7 +1159,7 @@ export function fetchKeys( count: number, telemetryProperties?: {}, }, - onSuccess?: () => void, + onSuccess?: (data: GetKeysWithDetailsResponse[] | GetKeysWithDetailsResponse) => void, onFailed?: () => void, ) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 34ab199677..62abf3c875 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -266,5 +266,6 @@ export enum TelemetryEvent { TRIGGERS_AND_FUNCTIONS_LIBRARY_FAILED = 'TRIGGERS_AND_FUNCTIONS_LIBRARY_FAILED', TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_REQUESTED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_REQUESTED', TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CLICKED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CLICKED', - TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CANCELLED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CANCELLED' + TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CANCELLED = 'TRIGGERS_AND_FUNCTIONS_FUNCTION_INVOKE_CANCELLED', + TRIGGERS_AND_FUNCTIONS_FIND_KEY_CLICKED = 'TRIGGERS_AND_FUNCTIONS_FIND_KEY_CLICKED', } From 8b42287c653d346b6f4254574f22392f82eba547 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 7 Jul 2023 13:54:06 +0200 Subject: [PATCH 046/106] comment fix --- .../components/monaco-editor/MonacoEditor.tsx | 5 ++- .../components/bottom-panel/cli.ts | 8 ++++ .../triggers-and-functions-functions-page.ts | 38 ++++++++++++++++++- .../triggers-and-functions-libraries-page.ts | 2 +- .../invoke_function.txt | 6 +++ .../triggers-and-functions/libraries.e2e.ts | 28 +++++++++++++- 6 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/test-data/triggers-and-functions/invoke_function.txt diff --git a/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx index a330c6d1b8..eebc7ab351 100644 --- a/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx +++ b/redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx @@ -47,7 +47,7 @@ const MonacoEditor = (props: Props) => { wrapperClassName, className, options = {}, - 'data-testid': dataTestId + 'data-testid': dataTestId = 'monaco-editor' } = props const [isEditing, setIsEditing] = useState(!readOnly && !disabled) @@ -112,7 +112,7 @@ const MonacoEditor = (props: Props) => { declineOnUnmount={false} preventOutsideClick > -
+
{ options={monacoOptions} className={cx(styles.editor, className, { readMode: !isEditing && readOnly })} editorDidMount={editorDidMount} + data-testid={dataTestId} />
diff --git a/tests/e2e/pageObjects/components/bottom-panel/cli.ts b/tests/e2e/pageObjects/components/bottom-panel/cli.ts index 0b766d2e0b..a4d277617c 100644 --- a/tests/e2e/pageObjects/components/bottom-panel/cli.ts +++ b/tests/e2e/pageObjects/components/bottom-panel/cli.ts @@ -170,4 +170,12 @@ export class Cli { const executedCommand = await this.cliCommandExecuted.withExactText(command); return await executedCommand.nextSibling(0).textContent; } + + /** + * Get executed text by index + * @param index index of the command in the CLI, by default is the last one + */ + async getExecutedCommandTextByIndex(index = -1): Promise { + return await this.cliCommandExecuted.nth(index).textContent; + } } diff --git a/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts b/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts index cddccfb75c..ba283d57b3 100644 --- a/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts +++ b/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts @@ -1,14 +1,23 @@ -import { Selector } from 'testcafe'; +import { Selector, t } from 'testcafe'; import { FunctionsSections } from '../helpers/constants'; import { InstancePage } from './instance-page'; export class TriggersAndFunctionsFunctionsPage extends InstancePage { + //Links librariesLink = Selector('[data-testid=triggered-functions-tab-libraries]'); + //Buttons + invokeButton = Selector('[data-testid=invoke-btn]'); + addArgumentItemButton = Selector('[data-testid=add-new-argument-item]'); + addKeyNameItemButton = Selector('[data-testid=add-new-key-item]'); + runInCliButton = Selector('[data-testid=invoke-function-btn]'); + //Masks // insert name functionNameMask = '[data-testid=row-$name]'; sectionMask = '[data-testid^=function-details-$name]'; + argumentRowMask = '[data-testid=argument-field-$index]'; + keyNameRowMask = '[data-testid=keyname-field-$index]'; /** * get function by name @@ -25,4 +34,31 @@ export class TriggersAndFunctionsFunctionsPage extends InstancePage { async getFieldsAndValuesBySection(sectionName: FunctionsSections): Promise { return Selector(this.sectionMask.replace(/\$name/g, sectionName)).textContent; } + + /**0 + * Enter function arguments + * @param args function arguments + */ + async enterFunctionArguments(args: string[]): Promise { + for (let i = 0; i < args.length; i++) { + if (i > 0) { + await t.click(this.addArgumentItemButton); + } + const input = Selector(this.argumentRowMask.replace(/\$index/g, i.toString())); + await t.typeText(input, args[i]); + } + } + /** + * Enter function key name + * @param args key names + */ + async enterFunctionKeyName(args: string[]): Promise { + for(let i = 0; i < args.length; i++) { + if (i > 0) { + await t.click(this.addKeyNameItemButton); + } + const input = Selector(this.keyNameRowMask.replace(/\$index/g, i.toString())); + await t.typeText(input, args[i]); + } + } } diff --git a/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts b/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts index d6642103df..1fff94e7c7 100644 --- a/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts +++ b/tests/e2e/pageObjects/triggers-and-functions-libraries-page.ts @@ -29,7 +29,7 @@ export class TriggersAndFunctionsLibrariesPage extends InstancePage { deleteMask = '[data-testid=delete-library-$name]'; sectionMask = '[data-testid^=functions-$name]'; functionMask = '[data-testid=func-$name]'; - inputMonaco = '[data-testid=$name]'; + inputMonaco = '[data-testid=wrapper-$name]'; /** * Is library displayed in the table diff --git a/tests/e2e/test-data/triggers-and-functions/invoke_function.txt b/tests/e2e/test-data/triggers-and-functions/invoke_function.txt new file mode 100644 index 0000000000..3baac1e4a5 --- /dev/null +++ b/tests/e2e/test-data/triggers-and-functions/invoke_function.txt @@ -0,0 +1,6 @@ +#!js api_version=1.0 name=lib + +redis.registerFunction('function', function(client, word1, word2, w3rd1){ + + return '${word1 ${word2} ${word3}'; +}); diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index c7e1573c19..78cafd3d6e 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -16,7 +16,11 @@ const triggersAndFunctionsLibrariesPage = new TriggersAndFunctionsLibrariesPage( const triggersAndFunctionsFunctionsPage = new TriggersAndFunctionsFunctionsPage(); const libraryName = 'lib'; -const filePath = path.join('..', '..', '..', 'test-data', 'triggers-and-functions', 'library.txt'); + +const filePathes = { + upload: path.join('..', '..', '..', 'test-data', 'triggers-and-functions', 'library.txt'), + invoke: path.join('..', '..', '..', 'test-data', 'triggers-and-functions', 'invoke_function.txt') +}; const LIBRARIES_LIST = [ { name: 'Function1', type: LibrariesSections.Functions }, @@ -134,7 +138,7 @@ test('Verify that library can be uploaded', async t => { await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); await t.click(triggersAndFunctionsFunctionsPage.librariesLink); await t.click(triggersAndFunctionsLibrariesPage.addLibraryButton); - await t.setFilesToUpload(triggersAndFunctionsLibrariesPage.uploadInput, [filePath]); + await t.setFilesToUpload(triggersAndFunctionsLibrariesPage.uploadInput, [filePathes.upload]); const uploadedText = await triggersAndFunctionsLibrariesPage.getTextFromMonaco(); await t.expect(uploadedText.length).gte(1, 'file was not uploaded'); await CommonElementsActions.checkCheckbox(triggersAndFunctionsLibrariesPage.addConfigurationCheckBox, true); @@ -144,3 +148,23 @@ test('Verify that library can be uploaded', async t => { await t.expect(triggersAndFunctionsLibrariesPage.getFunctionsByName(LibrariesSections.Functions, functionNameFromFile).exists).ok('the library information was not opened'); }); +test('Verify that function can be invoked', async t => { + const functionNameFromFile = 'function'; + const keyName = ['Hello']; + const argumentsName = ['world', '!!!' ]; + const expectedCommand = `TFCALL "${libraryName}.${functionNameFromFile}" "${keyName.length}" "${keyName}" "${argumentsName.join('" "')}"`; + + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + await t.click(triggersAndFunctionsLibrariesPage.addLibraryButton); + await t.setFilesToUpload(triggersAndFunctionsLibrariesPage.uploadInput, [filePathes.invoke]); + await t.click(triggersAndFunctionsLibrariesPage.addLibrarySubmitButton); + await t.click(triggersAndFunctionsLibrariesPage.functionsLink); + await t.click(triggersAndFunctionsFunctionsPage.getFunctionsNameSelector(functionNameFromFile)); + await t.click(triggersAndFunctionsFunctionsPage.invokeButton); + await triggersAndFunctionsFunctionsPage.enterFunctionArguments(argumentsName); + await triggersAndFunctionsFunctionsPage.enterFunctionKeyName(keyName); + await t.click(triggersAndFunctionsFunctionsPage.runInCliButton); + await t.expect(await triggersAndFunctionsFunctionsPage.Cli.getExecutedCommandTextByIndex()).eql(expectedCommand); + await t.click(triggersAndFunctionsFunctionsPage.Cli.cliCollapseButton); +}); From ff2c1b864d28ea8fb663210258095ff18165fcb9 Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Fri, 7 Jul 2023 15:24:36 +0300 Subject: [PATCH 047/106] #RI-4596 - add recommendations for triggered and functions (#2283) * #RI-4596 - add recommendations for triggered and functions --- .../src/common/constants/recommendations.ts | 15 +- .../api/src/constants/recommendations.ts | 5 +- .../api/src/constants/redis-modules.ts | 12 +- .../keys-business.service.spec.ts | 11 +- .../keys-business/keys-business.service.ts | 7 + .../database-analysis.service.ts | 1 + .../database-analysis/scanner/keys-scanner.ts | 12 +- .../scanner/recommendation.provider.ts | 6 + .../functions-with-keyspace.strategy.spec.ts | 113 +++++++++ .../functions-with-keyspace.strategy.ts | 45 ++++ .../functions-with-streams.strategy.spec.ts | 110 +++++++++ .../functions-with-streams.strategy.ts | 42 ++++ .../scanner/strategies/index.ts | 3 + .../lua-to-functions.strategy.spec.ts | 98 ++++++++ .../strategies/lua-to-functions.strategy.ts | 41 ++++ .../database-connection.service.spec.ts | 12 +- .../database/database-connection.service.ts | 10 + .../providers/recommendation.provider.spec.ts | 147 ++++++++---- .../providers/recommendation.provider.ts | 91 +++++++- .../recommendation/recommendation.service.ts | 17 +- .../triggered-functions/models/index.ts | 1 - .../triggered-functions/models/library.ts | 8 +- .../models/short-function.ts | 6 - .../models/short-library.ts | 6 +- .../src/utils/recommendation-helper.spec.ts | 22 +- .../api/src/utils/recommendation-helper.ts | 10 + .../POST-databases-id-analysis.test.ts | 64 ++++++ redisinsight/api/test/helpers/constants.ts | 12 + .../constants/dbAnalysisRecommendations.json | 215 ++++++++++++++++++ 29 files changed, 1071 insertions(+), 71 deletions(-) create mode 100644 redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-keyspace.strategy.spec.ts create mode 100644 redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-keyspace.strategy.ts create mode 100644 redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-streams.strategy.spec.ts create mode 100644 redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-streams.strategy.ts create mode 100644 redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.spec.ts create mode 100644 redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.ts delete mode 100644 redisinsight/api/src/modules/triggered-functions/models/short-function.ts diff --git a/redisinsight/api/src/common/constants/recommendations.ts b/redisinsight/api/src/common/constants/recommendations.ts index fa05965931..d17d1d6346 100644 --- a/redisinsight/api/src/common/constants/recommendations.ts +++ b/redisinsight/api/src/common/constants/recommendations.ts @@ -1,11 +1,11 @@ export enum SearchVisualizationCommands { - FT_INFO = "FT.INFO", - FT_SEARCH = "FT.SEARCH", - FT_AGGREGATE = "FT.AGGREGATE", - FT_PROFILE = "FT.PROFILE", - FT_EXPLAIN = "FT.EXPLAIN", - TS_RANGE = "TS.RANGE", - TS_MRANGE = "TS.MRANGE", + FT_INFO = 'FT.INFO', + FT_SEARCH = 'FT.SEARCH', + FT_AGGREGATE = 'FT.AGGREGATE', + FT_PROFILE = 'FT.PROFILE', + FT_EXPLAIN = 'FT.EXPLAIN', + TS_RANGE = 'TS.RANGE', + TS_MRANGE = 'TS.MRANGE', } export const LUA_SCRIPT_RECOMMENDATION_COUNT = 10; @@ -23,3 +23,4 @@ export const COMBINE_SMALL_STRINGS_TO_HASHES_RECOMMENDATION_KEYS_COUNT = 10; export const SEARCH_HASH_RECOMMENDATION_KEYS_FOR_CHECK = 50; export const SEARCH_HASH_RECOMMENDATION_KEYS_LENGTH = 2; export const RTS_KEYS_FOR_CHECK = 100; +export const LUA_TO_FUNCTIONS_RECOMMENDATION_COUNT = 1; diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index dda37a3a3a..8556cd18aa 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -1,5 +1,6 @@ export const RECOMMENDATION_NAMES = Object.freeze({ LUA_SCRIPT: 'luaScript', + LUA_TO_FUNCTIONS: 'luaToFunctions', BIG_HASHES: 'bigHashes', BIG_STRINGS: 'bigStrings', BIG_SETS: 'bigSets', @@ -20,6 +21,8 @@ export const RECOMMENDATION_NAMES = Object.freeze({ STRING_TO_JSON: 'stringToJson', SEARCH_VISUALIZATION: 'searchVisualization', SEARCH_HASH: 'searchHash', + FUNCTIONS_WITH_KEYSPACE: 'functionsWithKeyspace', + FUNCTIONS_WITH_STREAMS: 'functionsWithStreams', }); export const ONE_NODE_RECOMMENDATIONS = [ @@ -37,4 +40,4 @@ export const REDIS_STACK = [ RECOMMENDATION_NAMES.SEARCH_INDEXES, RECOMMENDATION_NAMES.SEARCH_JSON, RECOMMENDATION_NAMES.STRING_TO_JSON, -] +]; diff --git a/redisinsight/api/src/constants/redis-modules.ts b/redisinsight/api/src/constants/redis-modules.ts index d77f41de03..7a1b5e8f9c 100644 --- a/redisinsight/api/src/constants/redis-modules.ts +++ b/redisinsight/api/src/constants/redis-modules.ts @@ -14,6 +14,11 @@ export enum AdditionalSearchModuleName { FTL = 'ftl', } +export enum AdditionalTriggeredAndFunctionsModuleName { + RedisGears = 'redisgears', + RedisGears2 = 'redisgears_2', +} + export const SUPPORTED_REDIS_MODULES = Object.freeze({ ai: AdditionalRedisModuleName.RedisAI, graph: AdditionalRedisModuleName.RedisGraph, @@ -59,4 +64,9 @@ export const REDISEARCH_MODULES: string[] = [ AdditionalSearchModuleName.SearchLight, AdditionalSearchModuleName.FT, AdditionalSearchModuleName.FTL, -] +]; + +export const TRIGGERED_AND_FUNCTIONS_MODULES: string[] = [ + AdditionalTriggeredAndFunctionsModuleName.RedisGears, + AdditionalTriggeredAndFunctionsModuleName.RedisGears2, +]; diff --git a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts index 0b719ce447..57b4206a14 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts @@ -224,12 +224,19 @@ describe('KeysBusinessService', () => { { keys: [getKeyInfoResponse.name] }, ); + expect(recommendationService.check).toBeCalledTimes(2); expect(recommendationService.check).toBeCalledWith( mockBrowserClientMetadata, RECOMMENDATION_NAMES.SEARCH_JSON, { keys: result, client: nodeClient, databaseId: mockBrowserClientMetadata.databaseId }, ); - expect(recommendationService.check).toBeCalledTimes(1); + expect(recommendationService.check).toBeCalledWith( + mockBrowserClientMetadata, + RECOMMENDATION_NAMES.FUNCTIONS_WITH_STREAMS, + { keys: result, client: nodeClient, databaseId: mockBrowserClientMetadata.databaseId }, + ); + + expect(recommendationService.check).toBeCalledTimes(2); }); it("user don't have required permissions for getKeyInfo", async () => { const replyError: ReplyError = { @@ -322,7 +329,7 @@ describe('KeysBusinessService', () => { expect(browserHistory.create).not.toHaveBeenCalled(); }); it('should call recommendationService', async () => { - const response = [mockGetKeysWithDetailsResponse] + const response = [mockGetKeysWithDetailsResponse]; standaloneScanner.getKeys = jest .fn() .mockResolvedValue(response); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts index 09e7cfb9c1..33f3fdb7fc 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts @@ -184,11 +184,18 @@ export class KeysBusinessService { const client = await this.browserTool.getRedisClient(clientMetadata); const scanner = this.scanner.getStrategy(client.isCluster ? ConnectionType.CLUSTER : ConnectionType.STANDALONE); const result = await scanner.getKeysInfo(client, dto.keys, dto.type); + this.recommendationService.check( clientMetadata, RECOMMENDATION_NAMES.SEARCH_JSON, { keys: result, client, databaseId: clientMetadata.databaseId }, ); + this.recommendationService.check( + clientMetadata, + RECOMMENDATION_NAMES.FUNCTIONS_WITH_STREAMS, + { keys: result, client, databaseId: clientMetadata.databaseId }, + ); + return plainToClass(GetKeyInfoResponse, result); } catch (error) { this.logger.error(`Failed to get keys info: ${error.message}.`); diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index 2a70d6bf7d..41c8ee8bf1 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -64,6 +64,7 @@ export class DatabaseAnalysisService { client: nodeResult.client, keys: nodeResult.keys, indexes: nodeResult.indexes, + libraries: nodeResult.libraries, total: progress.total, globalClient: client, exclude: recommendationToExclude, diff --git a/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.ts b/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.ts index cc73145e2e..e58f47d99e 100644 --- a/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.ts +++ b/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.ts @@ -23,7 +23,8 @@ export class KeysScanner { async nodeScan(client: Redis, opts: any) { const total = await getTotal(client); - let indexes; + let indexes: string[]; + let libraries: string[]; try { indexes = await client.sendCommand( @@ -33,6 +34,14 @@ export class KeysScanner { // Ignore errors } + try { + libraries = await client.sendCommand( + new Command('TFUNCTION', ['LIST'], { replyEncoding: 'utf8' }), + ) as string[]; + } catch (err) { + // Ignore errors + } + const [ , keys, @@ -77,6 +86,7 @@ export class KeysScanner { return { keys: nodeKeys, indexes, + libraries, progress: { total, scanned: opts.filter.count, diff --git a/redisinsight/api/src/modules/database-recommendation/scanner/recommendation.provider.ts b/redisinsight/api/src/modules/database-recommendation/scanner/recommendation.provider.ts index 96ecaf6a1c..ae29534a8b 100644 --- a/redisinsight/api/src/modules/database-recommendation/scanner/recommendation.provider.ts +++ b/redisinsight/api/src/modules/database-recommendation/scanner/recommendation.provider.ts @@ -18,6 +18,9 @@ import { BigStringStrategy, CompressionForListStrategy, BigAmountConnectedClientsStrategy, + FunctionsWithStreamsStrategy, + FunctionsWithKeyspaceStrategy, + LuaToFunctionsStrategy, } from 'src/modules/database-recommendation/scanner/strategies'; @Injectable() @@ -43,6 +46,9 @@ export class RecommendationProvider { this.strategies.set(RECOMMENDATION_NAMES.BIG_STRINGS, new BigStringStrategy()); this.strategies.set(RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST, new CompressionForListStrategy()); this.strategies.set(RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS, new BigAmountConnectedClientsStrategy()); + this.strategies.set(RECOMMENDATION_NAMES.FUNCTIONS_WITH_STREAMS, new FunctionsWithStreamsStrategy(databaseService)); + this.strategies.set(RECOMMENDATION_NAMES.LUA_TO_FUNCTIONS, new LuaToFunctionsStrategy(databaseService)); + this.strategies.set(RECOMMENDATION_NAMES.FUNCTIONS_WITH_KEYSPACE, new FunctionsWithKeyspaceStrategy(databaseService)); } getStrategy(type: string): IRecommendationStrategy { diff --git a/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-keyspace.strategy.spec.ts b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-keyspace.strategy.spec.ts new file mode 100644 index 0000000000..bc344cabe8 --- /dev/null +++ b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-keyspace.strategy.spec.ts @@ -0,0 +1,113 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { when } from 'jest-when'; +import IORedis from 'ioredis'; +import { mockDatabaseService } from 'src/__mocks__'; +import { DatabaseService } from 'src/modules/database/database.service'; +import { FunctionsWithKeyspaceStrategy } from 'src/modules/database-recommendation/scanner/strategies'; + +const nodeClient = Object.create(IORedis.prototype); +nodeClient.sendCommand = jest.fn(); + +const mockDatabaseId = 'id'; + +const mockEmptyLibraries = []; +const mockLibraries = ['library']; + +const mockResponseWithTriggers = ['notify-keyspace-events', 'KEA']; +const mockResponseWithoutTriggers = ['notify-keyspace-events', 'X']; + +describe('FunctionsWithKeyspaceStrategy', () => { + let strategy; + let databaseService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: DatabaseService, + useFactory: mockDatabaseService, + }, + ], + }).compile(); + + databaseService = module.get(DatabaseService); + strategy = new FunctionsWithKeyspaceStrategy(databaseService); + }); + + describe('isRecommendationReached', () => { + describe('with triggered and functions module', () => { + beforeEach(() => { + databaseService.get.mockResolvedValue({ modules: [{ name: 'redisgears' }] }); + }); + + it('should return true when there is keyspace notification', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'TFUNCTION' })) + .mockResolvedValueOnce(mockEmptyLibraries); + + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'CONFIG' })) + .mockResolvedValueOnce(mockResponseWithTriggers); + + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + })).toEqual({ isReached: true }); + }); + + it('should return false when there is no keyspace notifications', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'TFUNCTION' })) + .mockResolvedValueOnce(mockEmptyLibraries); + + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'CONFIG' })) + .mockResolvedValueOnce(mockResponseWithoutTriggers); + + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + })).toEqual({ isReached: false }); + }); + + it('should return false when TFUNCTION return libraries', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'TFUNCTION' })) + .mockResolvedValueOnce(mockLibraries); + + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + })).toEqual({ isReached: false }); + }); + }); + + describe('without triggered and functions module', () => { + beforeEach(() => { + databaseService.get.mockResolvedValue({ modules: ['custom'] }); + }); + + it('should return true when there is keyspace notification', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'CONFIG' })) + .mockResolvedValueOnce(mockResponseWithTriggers); + + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + })).toEqual({ isReached: true }); + }); + + it('should return false when there is no keyspace notifications', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'CONFIG' })) + .mockResolvedValueOnce(mockResponseWithoutTriggers); + + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + })).toEqual({ isReached: false }); + }); + }); + }); +}); diff --git a/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-keyspace.strategy.ts b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-keyspace.strategy.ts new file mode 100644 index 0000000000..a4334ab9f7 --- /dev/null +++ b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-keyspace.strategy.ts @@ -0,0 +1,45 @@ +import { Command } from 'ioredis'; +import { AbstractRecommendationStrategy } + from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy'; +import { IDatabaseRecommendationStrategyData } + from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface'; +import { DatabaseService } from 'src/modules/database/database.service'; +import { isTriggeredAndFunctionsModule, checkKeyspaceNotification } from 'src/utils'; + +export class FunctionsWithKeyspaceStrategy extends AbstractRecommendationStrategy { + private databaseService: DatabaseService; + + constructor( + databaseService: DatabaseService, + ) { + super(); + this.databaseService = databaseService; + } + + /** + * Check functions with keyspace recommendation + * @param data + */ + + async isRecommendationReached( + data, + ): Promise { + const { modules } = await this.databaseService.get(data.databaseId); + + if (isTriggeredAndFunctionsModule(modules)) { + const libraries = await data.client.sendCommand( + new Command('TFUNCTION', ['LIST'], { replyEncoding: 'utf8' }), + ) as string[]; + + if (libraries.length) { + return { isReached: false }; + } + } + + const reply = await data.client.sendCommand( + new Command('CONFIG', ['GET', 'notify-keyspace-events'], { replyEncoding: 'utf8' }), + ) as string[]; + + return { isReached: checkKeyspaceNotification(reply[1]) }; + } +} diff --git a/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-streams.strategy.spec.ts b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-streams.strategy.spec.ts new file mode 100644 index 0000000000..221575592e --- /dev/null +++ b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-streams.strategy.spec.ts @@ -0,0 +1,110 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { when } from 'jest-when'; +import IORedis from 'ioredis'; +import { mockDatabaseService } from 'src/__mocks__'; +import { DatabaseService } from 'src/modules/database/database.service'; +import { GetKeyInfoResponse } from 'src/modules/browser/dto'; +import { FunctionsWithStreamsStrategy } from 'src/modules/database-recommendation/scanner/strategies'; + +const nodeClient = Object.create(IORedis.prototype); +nodeClient.sendCommand = jest.fn(); + +const mockDatabaseId = 'id'; + +const mockStreamInfo: GetKeyInfoResponse = { + name: Buffer.from('testString_1'), + type: 'stream', + ttl: -1, + size: 1, +}; + +const mockHashInfo: GetKeyInfoResponse = { + name: Buffer.from('testString_2'), + type: 'hash', + ttl: -1, + size: 1, +}; + +const mockEmptyLibraries = []; +const mockLibraries = ['library']; + +describe('FunctionsWithStreamsStrategy', () => { + let strategy; + let databaseService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: DatabaseService, + useFactory: mockDatabaseService, + }, + ], + }).compile(); + + databaseService = module.get(DatabaseService); + strategy = new FunctionsWithStreamsStrategy(databaseService); + }); + + describe('isRecommendationReached', () => { + describe('with triggered and functions module', () => { + beforeEach(() => { + databaseService.get.mockResolvedValue({ modules: [{ name: 'redisgears' }] }); + }); + + it('should return true when there is stream key', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'TFUNCTION' })) + .mockResolvedValue(mockEmptyLibraries); + + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + keys: [mockStreamInfo, mockHashInfo], + })).toEqual({ isReached: true }); + }); + + it('should return false when there is not stream key', async () => { + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + keys: [mockHashInfo], + })).toEqual({ isReached: false }); + }); + + it('should return false when TFUNCTION return libraries', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'TFUNCTION' })) + .mockResolvedValue(mockLibraries); + + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + keys: [mockStreamInfo, mockHashInfo], + })).toEqual({ isReached: false }); + }); + }); + + describe('without triggered and functions module', () => { + beforeEach(() => { + databaseService.get.mockResolvedValue({ modules: ['custom'] }); + }); + + it('should return true when there is stream key', async () => { + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + keys: [mockStreamInfo, mockHashInfo], + })).toEqual({ isReached: true }); + }); + + it('should return false when there is not stream key', async () => { + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + keys: [mockHashInfo], + })).toEqual({ isReached: false }); + }); + }); + }); +}); diff --git a/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-streams.strategy.ts b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-streams.strategy.ts new file mode 100644 index 0000000000..23e3c220ee --- /dev/null +++ b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/functions-with-streams.strategy.ts @@ -0,0 +1,42 @@ +import { Command } from 'ioredis'; +import { AbstractRecommendationStrategy } + from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy'; +import { IDatabaseRecommendationStrategyData } + from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface'; +import { DatabaseService } from 'src/modules/database/database.service'; +import { RedisDataType, GetKeyInfoResponse } from 'src/modules/browser/dto'; +import { isTriggeredAndFunctionsModule } from 'src/utils'; + +export class FunctionsWithStreamsStrategy extends AbstractRecommendationStrategy { + private databaseService: DatabaseService; + + constructor( + databaseService: DatabaseService, + ) { + super(); + this.databaseService = databaseService; + } + + /** + * Check functions with streams recommendation + * @param data + */ + + async isRecommendationReached( + data, + ): Promise { + const { modules } = await this.databaseService.get(data.databaseId); + + if (isTriggeredAndFunctionsModule(modules)) { + const libraries = await data.client.sendCommand( + new Command('TFUNCTION', ['LIST'], { replyEncoding: 'utf8' }), + ) as string[]; + + if (libraries.length) { + return { isReached: false }; + } + } + const isStream = data.keys.some((key: GetKeyInfoResponse) => key.type === RedisDataType.Stream); + return { isReached: isStream }; + } +} diff --git a/redisinsight/api/src/modules/database-recommendation/scanner/strategies/index.ts b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/index.ts index cd00f468bc..23a2e210fd 100644 --- a/redisinsight/api/src/modules/database-recommendation/scanner/strategies/index.ts +++ b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/index.ts @@ -13,3 +13,6 @@ export * from './avoid-lua-scripts.strategy'; export * from './big-string.strategy'; export * from './compression-for-list.strategy'; export * from './big-amount-connected-clients.strategy'; +export * from './functions-with-streams.strategy'; +export * from './lua-to-functions.strategy'; +export * from './functions-with-keyspace.strategy'; diff --git a/redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.spec.ts b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.spec.ts new file mode 100644 index 0000000000..53d242dcbf --- /dev/null +++ b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.spec.ts @@ -0,0 +1,98 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { when } from 'jest-when'; +import IORedis from 'ioredis'; +import { mockDatabaseService } from 'src/__mocks__'; +import { DatabaseService } from 'src/modules/database/database.service'; +import { LuaToFunctionsStrategy } from 'src/modules/database-recommendation/scanner/strategies'; + +const nodeClient = Object.create(IORedis.prototype); +nodeClient.sendCommand = jest.fn(); + +const mockDatabaseId = 'id'; + +const mockEmptyLibraries = []; +const mockLibraries = ['library']; + +describe('LuaToFunctionsStrategy', () => { + let strategy; + let databaseService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: DatabaseService, + useFactory: mockDatabaseService, + }, + ], + }).compile(); + + databaseService = module.get(DatabaseService); + strategy = new LuaToFunctionsStrategy(databaseService); + }); + + describe('isRecommendationReached', () => { + describe('with triggered and functions module', () => { + beforeEach(() => { + databaseService.get.mockResolvedValue({ modules: [{ name: 'redisgears' }] }); + }); + + it('should return true when there is more then 1 lua script', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'TFUNCTION' })) + .mockResolvedValue(mockEmptyLibraries); + + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + info: { cashedScripts: 2 }, + })).toEqual({ isReached: true }); + }); + + it('should return false when number of cached lua script is 1', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'TFUNCTION' })) + .mockResolvedValue(mockEmptyLibraries); + + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + info: { cashedScripts: 1 }, + })).toEqual({ isReached: false }); + }); + + it('should return false when TFUNCTION return libraries', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'TFUNCTION' })) + .mockResolvedValue(mockLibraries); + + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + })).toEqual({ isReached: false }); + }); + }); + + describe('without triggered and functions module', () => { + beforeEach(() => { + databaseService.get.mockResolvedValue({ modules: ['custom'] }); + }); + + it('should return true when there is more then 1 lua script', async () => { + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + info: { cashedScripts: 2 }, + })).toEqual({ isReached: true }); + }); + + it('should return false when number of cached lua script is 1', async () => { + expect(await strategy.isRecommendationReached({ + client: nodeClient, + databaseId: mockDatabaseId, + info: { cashedScripts: 1 }, + })).toEqual({ isReached: false }); + }); + }); + }); +}); diff --git a/redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.ts b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.ts new file mode 100644 index 0000000000..4fa3030515 --- /dev/null +++ b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.ts @@ -0,0 +1,41 @@ +import { Command } from 'ioredis'; +import { AbstractRecommendationStrategy } + from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy'; +import { IDatabaseRecommendationStrategyData } + from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface'; +import { DatabaseService } from 'src/modules/database/database.service'; +import { isTriggeredAndFunctionsModule } from 'src/utils'; +import { LUA_TO_FUNCTIONS_RECOMMENDATION_COUNT } from 'src/common/constants'; + +export class LuaToFunctionsStrategy extends AbstractRecommendationStrategy { + private databaseService: DatabaseService; + + constructor( + databaseService: DatabaseService, + ) { + super(); + this.databaseService = databaseService; + } + + /** + * Check lua to functions recommendation + * @param data + */ + + async isRecommendationReached( + data, + ): Promise { + const { modules } = await this.databaseService.get(data.databaseId); + + if (isTriggeredAndFunctionsModule(modules)) { + const libraries = await data.client.sendCommand( + new Command('TFUNCTION', ['LIST'], { replyEncoding: 'utf8' }), + ) as string[]; + + if (libraries.length) { + return { isReached: false }; + } + } + return { isReached: data.info.cashedScripts > LUA_TO_FUNCTIONS_RECOMMENDATION_COUNT }; + } +} diff --git a/redisinsight/api/src/modules/database/database-connection.service.spec.ts b/redisinsight/api/src/modules/database/database-connection.service.spec.ts index 5a8ca67bf5..e45229a724 100644 --- a/redisinsight/api/src/modules/database/database-connection.service.spec.ts +++ b/redisinsight/api/src/modules/database/database-connection.service.spec.ts @@ -87,7 +87,7 @@ describe('DatabaseConnectionService', () => { it('should call recommendationService', async () => { expect(await service.connect(mockCommonClientMetadata)).toEqual(undefined); - expect(recommendationService.check).toHaveBeenCalledTimes(3); + expect(recommendationService.check).toHaveBeenCalledTimes(5); expect(recommendationService.check).toBeCalledWith( mockCommonClientMetadata, @@ -104,6 +104,16 @@ describe('DatabaseConnectionService', () => { RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS, mockRedisGeneralInfo, ); + expect(recommendationService.check).toBeCalledWith( + mockCommonClientMetadata, + RECOMMENDATION_NAMES.LUA_TO_FUNCTIONS, + { client: mockIORedisClient, databaseId: mockCommonClientMetadata.databaseId, info: mockRedisGeneralInfo }, + ); + expect(recommendationService.check).toBeCalledWith( + mockCommonClientMetadata, + RECOMMENDATION_NAMES.FUNCTIONS_WITH_KEYSPACE, + { client: mockIORedisClient, databaseId: mockCommonClientMetadata.databaseId }, + ); }); it('should call databaseInfoProvider', async () => { diff --git a/redisinsight/api/src/modules/database/database-connection.service.ts b/redisinsight/api/src/modules/database/database-connection.service.ts index ba946f5088..1ce668ad7d 100644 --- a/redisinsight/api/src/modules/database/database-connection.service.ts +++ b/redisinsight/api/src/modules/database/database-connection.service.ts @@ -78,6 +78,16 @@ export class DatabaseConnectionService { RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS, generalInfo, ); + this.recommendationService.check( + clientMetadata, + RECOMMENDATION_NAMES.LUA_TO_FUNCTIONS, + { client, databaseId: clientMetadata.databaseId, info: generalInfo }, + ); + this.recommendationService.check( + clientMetadata, + RECOMMENDATION_NAMES.FUNCTIONS_WITH_KEYSPACE, + { client, databaseId: clientMetadata.databaseId }, + ); this.logger.log(`Succeed to connect to database ${clientMetadata.databaseId}`); } diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 799b4ccca4..b0f662a702 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -8,34 +8,39 @@ const nodeClient = Object.create(IORedis.prototype); nodeClient.isCluster = false; nodeClient.sendCommand = jest.fn(); -const mockRedisMemoryInfoResponse_1: string = '# Memory\r\nnumber_of_cached_scripts:10\r\n'; -const mockRedisMemoryInfoResponse_2: string = '# Memory\r\nnumber_of_cached_scripts:11\r\n'; +const mockRedisMemoryInfoResponse1: string = '# Memory\r\nnumber_of_cached_scripts:10\r\n'; +const mockRedisMemoryInfoResponse2: string = '# Memory\r\nnumber_of_cached_scripts:11\r\n'; +const mockRedisMemoryInfoResponse3: string = '# Memory\r\nnumber_of_cached_scripts:1\r\n'; +const mockRedisMemoryInfoResponse4: string = '# Memory\r\nnumber_of_cached_scripts:2\r\n'; -const mockRedisKeyspaceInfoResponse_1: string = '# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n'; -const mockRedisKeyspaceInfoResponse_2: string = `# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n +const mockRedisKeyspaceInfoResponse1: string = '# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n'; +const mockRedisKeyspaceInfoResponse2: string = `# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n db1:keys=0,expires=0,avg_ttl=0\r\n`; -const mockRedisKeyspaceInfoResponse_3: string = `# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n +const mockRedisKeyspaceInfoResponse3: string = `# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n db2:keys=20,expires=0,avg_ttl=0\r\n`; const mockRedisConfigResponse = ['name', '512']; -const mockRedisClientsResponse_1: string = '# Clients\r\nconnected_clients:100\r\n'; -const mockRedisClientsResponse_2: string = '# Clients\r\nconnected_clients:101\r\n'; +const mockRedisClientsResponse1: string = '# Clients\r\nconnected_clients:100\r\n'; +const mockRedisClientsResponse2: string = '# Clients\r\nconnected_clients:101\r\n'; -const mockRedisServerResponse_1: string = '# Server\r\nredis_version:6.0.0\r\n'; -const mockRedisServerResponse_2: string = '# Server\r\nredis_version:5.1.1\r\n'; +const mockRedisServerResponse1: string = '# Server\r\nredis_version:6.0.0\r\n'; +const mockRedisServerResponse2: string = '# Server\r\nredis_version:5.1.1\r\n'; -const mockRedisAclListResponse_1: string[] = [ +const mockRedisAclListResponse1: string[] = [ 'user { it('should not return luaScript recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) - .mockResolvedValue(mockRedisMemoryInfoResponse_1); + .mockResolvedValue(mockRedisMemoryInfoResponse1); const luaScriptRecommendation = await service.determineLuaScriptRecommendation(nodeClient); expect(luaScriptRecommendation).toEqual(null); @@ -163,7 +169,7 @@ describe('RecommendationProvider', () => { it('should return luaScript recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) - .mockResolvedValue(mockRedisMemoryInfoResponse_2); + .mockResolvedValue(mockRedisMemoryInfoResponse2); const luaScriptRecommendation = await service.determineLuaScriptRecommendation(nodeClient); expect(luaScriptRecommendation).toEqual({ name: RECOMMENDATION_NAMES.LUA_SCRIPT }); @@ -208,7 +214,7 @@ describe('RecommendationProvider', () => { it('should not return avoidLogicalDatabases recommendation when only one logical db', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) - .mockResolvedValue(mockRedisKeyspaceInfoResponse_1); + .mockResolvedValue(mockRedisKeyspaceInfoResponse1); const avoidLogicalDatabasesRecommendation = await service.determineLogicalDatabasesRecommendation(nodeClient); expect(avoidLogicalDatabasesRecommendation).toEqual(null); @@ -217,7 +223,7 @@ describe('RecommendationProvider', () => { it('should not return avoidLogicalDatabases recommendation when only on logical db with keys', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) - .mockResolvedValue(mockRedisKeyspaceInfoResponse_2); + .mockResolvedValue(mockRedisKeyspaceInfoResponse2); const avoidLogicalDatabasesRecommendation = await service.determineLogicalDatabasesRecommendation(nodeClient); expect(avoidLogicalDatabasesRecommendation).toEqual(null); @@ -226,7 +232,7 @@ describe('RecommendationProvider', () => { it('should return avoidLogicalDatabases recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) - .mockResolvedValue(mockRedisKeyspaceInfoResponse_3); + .mockResolvedValue(mockRedisKeyspaceInfoResponse3); const avoidLogicalDatabasesRecommendation = await service.determineLogicalDatabasesRecommendation(nodeClient); expect(avoidLogicalDatabasesRecommendation).toEqual({ name: 'avoidLogicalDatabases' }); @@ -245,7 +251,7 @@ describe('RecommendationProvider', () => { nodeClient.isCluster = true; when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) - .mockResolvedValue(mockRedisKeyspaceInfoResponse_3); + .mockResolvedValue(mockRedisKeyspaceInfoResponse3); const avoidLogicalDatabasesRecommendation = await service.determineLogicalDatabasesRecommendation(nodeClient); expect(avoidLogicalDatabasesRecommendation).toEqual(null); @@ -267,7 +273,8 @@ describe('RecommendationProvider', () => { expect(smallStringRecommendation).toEqual(null); }); it('should return combineSmallStringsToHashes recommendation', async () => { - const smallStringRecommendation = await service.determineCombineSmallStringsToHashesRecommendation(new Array(10).fill(mockSmallStringKey)); + const smallStringRecommendation = await service + .determineCombineSmallStringsToHashesRecommendation(new Array(10).fill(mockSmallStringKey)); expect(smallStringRecommendation) .toEqual({ name: RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES, @@ -330,12 +337,12 @@ describe('RecommendationProvider', () => { .mockResolvedValue(mockRedisConfigResponse); const convertHashtableToZiplistRecommendation = await service - .determineHashHashtableToZiplistRecommendation(nodeClient, [...mockKeys, mockBigHashKey_3]); + .determineHashHashtableToZiplistRecommendation(nodeClient, [...mockKeys, mockBigHashKey3]); expect(convertHashtableToZiplistRecommendation) .toEqual( { name: RECOMMENDATION_NAMES.HASH_HASHTABLE_TO_ZIPLIST, - params: { keys: [mockBigHashKey_3.name] }, + params: { keys: [mockBigHashKey3.name] }, }, ); }); @@ -444,7 +451,7 @@ describe('RecommendationProvider', () => { it('should not return connectionClients recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) - .mockResolvedValue(mockRedisClientsResponse_1); + .mockResolvedValue(mockRedisClientsResponse1); const connectionClientsRecommendation = await service .determineConnectionClientsRecommendation(nodeClient); @@ -454,7 +461,7 @@ describe('RecommendationProvider', () => { it('should return connectionClients recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) - .mockResolvedValue(mockRedisClientsResponse_2); + .mockResolvedValue(mockRedisClientsResponse2); const connectionClientsRecommendation = await service .determineConnectionClientsRecommendation(nodeClient); @@ -478,7 +485,7 @@ describe('RecommendationProvider', () => { it('should not return setPassword recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'acl' })) - .mockResolvedValue(mockRedisAclListResponse_1); + .mockResolvedValue(mockRedisAclListResponse1); const setPasswordRecommendation = await service .determineSetPasswordRecommendation(nodeClient); @@ -488,7 +495,7 @@ describe('RecommendationProvider', () => { it('should return setPassword recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'acl' })) - .mockResolvedValue(mockRedisAclListResponse_2); + .mockResolvedValue(mockRedisAclListResponse2); const setPasswordRecommendation = await service .determineSetPasswordRecommendation(nodeClient); @@ -533,7 +540,7 @@ describe('RecommendationProvider', () => { it('should not return redis version recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) - .mockResolvedValue(mockRedisServerResponse_1); + .mockResolvedValue(mockRedisServerResponse1); const redisVersionRecommendation = await service .determineRedisVersionRecommendation(nodeClient); @@ -543,7 +550,7 @@ describe('RecommendationProvider', () => { it('should return redis version recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) - .mockResolvedValueOnce(mockRedisServerResponse_2); + .mockResolvedValueOnce(mockRedisServerResponse2); const redisVersionRecommendation = await service .determineRedisVersionRecommendation(nodeClient); @@ -566,13 +573,13 @@ describe('RecommendationProvider', () => { describe('determineSearchJSONRecommendation', () => { it('should not return searchJSON', async () => { const searchJSONRecommendation = await service - .determineSearchJSONRecommendation(mockKeys, mockFTListResponse_2); + .determineSearchJSONRecommendation(mockKeys, mockFTListResponse2); expect(searchJSONRecommendation).toEqual(null); }); it('should return searchJSON recommendation', async () => { const searchJSONRecommendation = await service - .determineSearchJSONRecommendation(mockKeys, mockFTListResponse_1); + .determineSearchJSONRecommendation(mockKeys, mockFTListResponse1); expect(searchJSONRecommendation) .toEqual({ name: RECOMMENDATION_NAMES.SEARCH_JSON, @@ -582,7 +589,7 @@ describe('RecommendationProvider', () => { it('should return not searchJSON recommendation when there is no JSON key', async () => { const searchJSONRecommendation = await service - .determineSearchJSONRecommendation([mockBigSet], mockFTListResponse_1); + .determineSearchJSONRecommendation([mockBigSet], mockFTListResponse1); expect(searchJSONRecommendation) .toEqual(null); }); @@ -634,13 +641,13 @@ describe('RecommendationProvider', () => { while (counter <= 100) { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'zscan' })) - .mockResolvedValueOnce(mockZScanResponse_1); + .mockResolvedValueOnce(mockZScanResponse1); counter += 1; } when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'zscan' })) - .mockResolvedValueOnce(mockZScanResponse_2); + .mockResolvedValueOnce(mockZScanResponse2); const RTSRecommendation = await service .determineRTSRecommendation(nodeClient, mockSortedSets); @@ -659,4 +666,62 @@ describe('RecommendationProvider', () => { expect(RTSRecommendation).toEqual(null); }); }); + + describe('determineLuaToFunctionsRecommendation', () => { + it('should return null when there are libraries', async () => { + const luaToFunctionsRecommendation = await service + .determineLuaToFunctionsRecommendation(nodeClient, mockTfunctionListResponse2); + expect(luaToFunctionsRecommendation).toEqual(null); + }); + + it('should return luaToFunctions recommendation when lua script > 1', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValueOnce(mockRedisMemoryInfoResponse4); + + const luaToFunctionsRecommendation = await service + .determineLuaToFunctionsRecommendation(nodeClient, mockTfunctionListResponse1); + expect(luaToFunctionsRecommendation).toEqual({ name: RECOMMENDATION_NAMES.LUA_TO_FUNCTIONS }); + }); + + it('should return null when lua script <= 1', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValueOnce(mockRedisMemoryInfoResponse3); + + const luaToFunctionsRecommendation = await service + .determineLuaToFunctionsRecommendation(nodeClient, mockTfunctionListResponse1); + expect(luaToFunctionsRecommendation).toEqual(null); + }); + + it('should return null when info command executed with error', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockRejectedValue('some error'); + + const luaToFunctionsRecommendation = await service + .determineLuaToFunctionsRecommendation(nodeClient, mockTfunctionListResponse1); + expect(luaToFunctionsRecommendation).toEqual(null); + }); + }); + + describe('determineFunctionsWithStreamsRecommendation', () => { + it('should return null when there are libraries', async () => { + const functionsWithStreamsRecommendation = await service + .determineFunctionsWithStreamsRecommendation(nodeClient, mockTfunctionListResponse2); + expect(functionsWithStreamsRecommendation).toEqual(null); + }); + + it('should return functionsWithStreams recommendation when there is stream key', async () => { + const functionsWithStreamsRecommendation = await service + .determineFunctionsWithStreamsRecommendation([mockStreamKey], mockTfunctionListResponse1); + expect(functionsWithStreamsRecommendation).toEqual({ name: RECOMMENDATION_NAMES.FUNCTIONS_WITH_STREAMS }); + }); + + it('should return null when there is no stream key', async () => { + const functionsWithStreamsRecommendation = await service + .determineFunctionsWithStreamsRecommendation([mockSmallStringKey], mockTfunctionListResponse1); + expect(functionsWithStreamsRecommendation).toEqual(null); + }); + }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index a47d3bebcb..e9c94377e8 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -2,7 +2,9 @@ import { Injectable, Logger } from '@nestjs/common'; import { Redis, Cluster, Command } from 'ioredis'; import { get } from 'lodash'; import * as semverCompare from 'node-version-compare'; -import { convertRedisInfoReplyToObject, convertBulkStringsToObject, checkTimestamp } from 'src/utils'; +import { + convertRedisInfoReplyToObject, convertBulkStringsToObject, checkTimestamp, checkKeyspaceNotification, +} from 'src/utils'; import { RECOMMENDATION_NAMES } from 'src/constants'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; @@ -24,6 +26,7 @@ import { SEARCH_HASH_RECOMMENDATION_KEYS_FOR_CHECK, SEARCH_HASH_RECOMMENDATION_KEYS_LENGTH, RTS_KEYS_FOR_CHECK, + LUA_TO_FUNCTIONS_RECOMMENDATION_COUNT, } from 'src/common/constants'; @Injectable() @@ -535,4 +538,90 @@ export class RecommendationProvider { return null; } } + + /** + * Check luaToFunctions recommendation + * @param redisClient + * @param libraries + */ + + async determineLuaToFunctionsRecommendation( + redisClient: Redis | Cluster, + libraries?: string[], + ): Promise { + if (libraries?.length) { + return null; + } + + try { + const info = convertRedisInfoReplyToObject( + await redisClient.sendCommand( + new Command('info', ['memory'], { replyEncoding: 'utf8' }), + ) as string, + ); + + const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts'); + + return parseInt(nodesNumbersOfCachedScripts, 10) > LUA_TO_FUNCTIONS_RECOMMENDATION_COUNT + ? { name: RECOMMENDATION_NAMES.LUA_TO_FUNCTIONS } + : null; + } catch (err) { + this.logger.error('Can not determine Lua to functions recommendation', err); + return null; + } + } + + /** + * Check functionsWithKeyspace recommendation + * @param redisClient + * @param libraries + */ + + async determineFunctionsWithKeyspaceRecommendation( + redisClient: Redis | Cluster, + libraries?: string[], + ): Promise { + if (libraries?.length) { + return null; + } + + try { + const info = await redisClient.sendCommand( + new Command('CONFIG', ['GET', 'notify-keyspace-events'], { replyEncoding: 'utf8' }), + ); + + return checkKeyspaceNotification(info[1]) + ? { name: RECOMMENDATION_NAMES.FUNCTIONS_WITH_KEYSPACE } + : null; + } catch (err) { + this.logger.error('Can not determine functions with keyspace recommendation', err); + return null; + } + } + + /** + * Check functionsWithStreams recommendation + * @param keys + * @param libraries + */ + + async determineFunctionsWithStreamsRecommendation( + keys: Key[], + libraries?: string[], + ): Promise { + if (libraries?.length) { + return null; + } + + try { + const isStreamKey = keys.some((key) => key.type === RedisDataType.Stream); + + return isStreamKey + ? { name: RECOMMENDATION_NAMES.FUNCTIONS_WITH_STREAMS } + : null; + } catch (err) { + this.logger.error('Can not determine functions with streams recommendation', err); + return null; + } + } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index a03ce2e67a..40c08e0c30 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -15,6 +15,7 @@ interface RecommendationInput { globalClient?: Redis | Cluster, exclude?: string[], indexes?: string[], + libraries?: string[], } @Injectable() @@ -39,9 +40,10 @@ export class RecommendationService { globalClient, exclude, indexes, + libraries, } = dto; - const recommendations = new Map Promise>([ + const recommendations: Map Promise> = new Map([ [ RECOMMENDATION_NAMES.LUA_SCRIPT, async () => await this.recommendationProvider.determineLuaScriptRecommendation(client), @@ -114,6 +116,19 @@ export class RecommendationService { RECOMMENDATION_NAMES.SEARCH_HASH, async () => await this.recommendationProvider.determineSearchHashRecommendation(keys, indexes), ], + [ + RECOMMENDATION_NAMES.LUA_TO_FUNCTIONS, + async () => await this.recommendationProvider.determineLuaToFunctionsRecommendation(client, libraries), + ], + [ + RECOMMENDATION_NAMES.FUNCTIONS_WITH_KEYSPACE, + async () => await this.recommendationProvider.determineFunctionsWithKeyspaceRecommendation(client, libraries), + ], + [ + RECOMMENDATION_NAMES.FUNCTIONS_WITH_STREAMS, + async () => await this.recommendationProvider + .determineFunctionsWithStreamsRecommendation(keys, libraries), + ], // it is live time recommendation (will add later) [ RECOMMENDATION_NAMES.STRING_TO_JSON, diff --git a/redisinsight/api/src/modules/triggered-functions/models/index.ts b/redisinsight/api/src/modules/triggered-functions/models/index.ts index 3120e8a147..d3cee0935b 100644 --- a/redisinsight/api/src/modules/triggered-functions/models/index.ts +++ b/redisinsight/api/src/modules/triggered-functions/models/index.ts @@ -1,4 +1,3 @@ export * from './function'; export * from './library'; -export * from './short-function'; export * from './short-library'; diff --git a/redisinsight/api/src/modules/triggered-functions/models/library.ts b/redisinsight/api/src/modules/triggered-functions/models/library.ts index f945390992..8d1d68ffc2 100644 --- a/redisinsight/api/src/modules/triggered-functions/models/library.ts +++ b/redisinsight/api/src/modules/triggered-functions/models/library.ts @@ -2,8 +2,10 @@ import { Expose, Type } from 'class-transformer'; import { IsArray, IsString, IsNumber, } from 'class-validator'; -import { ShortFunction } from 'src/modules/triggered-functions/models'; -import { ApiProperty } from '@nestjs/swagger'; +import { Function } from 'src/modules/triggered-functions/models'; +import { ApiProperty, PickType } from '@nestjs/swagger'; + +export class ShortFunction extends PickType(Function, ['name', 'type'] as const) {} export class Library { @ApiProperty({ @@ -65,8 +67,8 @@ export class Library { type: ShortFunction, }) @IsArray() - @Type(() => ShortFunction) @Expose() + @Type(() => ShortFunction) functions: ShortFunction[]; @ApiProperty({ diff --git a/redisinsight/api/src/modules/triggered-functions/models/short-function.ts b/redisinsight/api/src/modules/triggered-functions/models/short-function.ts deleted file mode 100644 index b589238ce5..0000000000 --- a/redisinsight/api/src/modules/triggered-functions/models/short-function.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PartialType, PickType } from '@nestjs/swagger'; -import { Function } from 'src/modules/triggered-functions/models'; - -export class ShortFunction extends PartialType( - PickType(Function, ['name', 'type'] as const), -) {} diff --git a/redisinsight/api/src/modules/triggered-functions/models/short-library.ts b/redisinsight/api/src/modules/triggered-functions/models/short-library.ts index 3475230a3d..c32f3d3a04 100644 --- a/redisinsight/api/src/modules/triggered-functions/models/short-library.ts +++ b/redisinsight/api/src/modules/triggered-functions/models/short-library.ts @@ -1,6 +1,4 @@ -import { PartialType, PickType } from '@nestjs/swagger'; +import { PickType } from '@nestjs/swagger'; import { Library } from 'src/modules/triggered-functions/models/library'; -export class ShortLibrary extends PartialType( - PickType(Library, ['name', 'user', 'totalFunctions', 'pendingJobs'] as const), -) {} +export class ShortLibrary extends PickType(Library, ['name', 'user', 'totalFunctions', 'pendingJobs'] as const) {} diff --git a/redisinsight/api/src/utils/recommendation-helper.spec.ts b/redisinsight/api/src/utils/recommendation-helper.spec.ts index 899d45c157..8cf4ad2d39 100644 --- a/redisinsight/api/src/utils/recommendation-helper.spec.ts +++ b/redisinsight/api/src/utils/recommendation-helper.spec.ts @@ -1,5 +1,7 @@ import { AdditionalSearchModuleName, AdditionalRedisModuleName } from 'src/constants'; -import { isRedisearchModule, sortRecommendations, checkTimestamp } from './recommendation-helper'; +import { + isRedisearchModule, sortRecommendations, checkTimestamp, checkKeyspaceNotification, +} from './recommendation-helper'; const nameToModule = (name: string) => ({ name }); @@ -82,6 +84,18 @@ const checkTimestampTests = [ { input: '-inf', expected: false }, ]; +const checkKeyspaceNotificationTests = [ + { input: '', expected: false }, + { input: 'fdKx', expected: true }, + { input: 'lsE', expected: true }, + { input: 'fdkx', expected: false }, + { input: 'lse', expected: false }, + { input: 'KfdE', expected: true }, + { input: '1', expected: false }, + { input: 'K', expected: true }, + { input: 'E', expected: true }, +]; + describe('Recommendation helper', () => { describe('isRedisearchModule', () => { it.each(getOutputForRedisearchAvailable)('for input: %s (reply), should be output: %s', @@ -106,4 +120,10 @@ describe('Recommendation helper', () => { expect(checkTimestamp(input)).toEqual(expected); }); }); + + describe('checkKeyspaceNotification', () => { + test.each(checkKeyspaceNotificationTests)('%j', ({ input, expected }) => { + expect(checkKeyspaceNotification(input)).toEqual(expected); + }); + }); }); diff --git a/redisinsight/api/src/utils/recommendation-helper.ts b/redisinsight/api/src/utils/recommendation-helper.ts index fa51ed2fda..c4f25c2475 100644 --- a/redisinsight/api/src/utils/recommendation-helper.ts +++ b/redisinsight/api/src/utils/recommendation-helper.ts @@ -5,6 +5,7 @@ import { REDISEARCH_MODULES, REDIS_STACK, RECOMMENDATION_NAMES, + TRIGGERED_AND_FUNCTIONS_MODULES, IS_TIMESTAMP, IS_INTEGER_NUMBER_REGEX, IS_NUMBER_REGEX, @@ -16,6 +17,10 @@ export const isRedisearchModule = (modules: AdditionalRedisModule[]): boolean => ({ name }) => REDISEARCH_MODULES.some((search) => name === search), ); +export const isTriggeredAndFunctionsModule = (modules: AdditionalRedisModule[]): boolean => modules?.some( + ({ name }) => TRIGGERED_AND_FUNCTIONS_MODULES.some((search) => name === search), +); + export const sortRecommendations = (recommendations: any[]) => sortBy(recommendations, [ ({ name }) => name !== RECOMMENDATION_NAMES.SEARCH_JSON, ({ name }) => name !== RECOMMENDATION_NAMES.SEARCH_INDEXES, @@ -43,3 +48,8 @@ export const checkTimestamp = (value: string): boolean => { return false; } }; + +// https://redis.io/docs/manual/keyspace-notifications/ +export const checkKeyspaceNotification = (reply: string): boolean => ( + reply.indexOf('K') > -1 || reply.indexOf('E') > -1 +); diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 2004398430..4157f320d3 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -368,6 +368,31 @@ describe('POST /databases/:instanceId/analysis', () => { }, ].map(mainCheckFn); }); + + describe('functionsWithKeyspace recommendation', () => { + requirements('!rte.pass'); + [ + { + name: 'Should create new database analysis with functionsWithKeyspace recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.sendCommand('CONFIG', ['set', 'notify-keyspace-events', 'KEA']); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.include.deep.members([ + constants.TEST_FUNCTIONS_WITH_KEYSPACE_RECOMMENDATION, + ]); + }, + after: async () => { + await rte.data.sendCommand('CONFIG', ['set', 'notify-keyspace-events', '']); + } + }, + ].map(mainCheckFn); + }); describe('searchHash recommendation', () => { requirements('!rte.pass'); @@ -656,6 +681,45 @@ describe('POST /databases/:instanceId/analysis', () => { expect(await repository.count()).to.eq(5); } }, + { + name: 'Should create new database analysis with luaToFunctions recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.generateNCachedScripts(2, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.include.deep.members([ + constants.TEST_LUA_TO_FUNCTIONS_RECOMMENDATION, + ]); + }, + after: async () => { + await rte.data.sendCommand('script', ['flush']); + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with functionsWithStreams recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.generateStreams(true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.include.deep.members([ + constants.TEST_FUNCTIONS_WITH_STREAMS_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, ].map(mainCheckFn); }); }); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 87c0a22f18..954f12db9a 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -558,6 +558,18 @@ export const constants = { name: RECOMMENDATION_NAMES.SEARCH_HASH, }, + TEST_LUA_TO_FUNCTIONS_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.LUA_TO_FUNCTIONS, + }, + + TEST_FUNCTIONS_WITH_STREAMS_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.FUNCTIONS_WITH_STREAMS, + }, + + TEST_FUNCTIONS_WITH_KEYSPACE_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.FUNCTIONS_WITH_KEYSPACE, + }, + TEST_LUA_SCRIPT_VOTE_RECOMMENDATION: { name: RECOMMENDATION_NAMES.LUA_SCRIPT, vote: 'useful', diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 0bc8c4803b..2a2cd0a301 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -1436,5 +1436,220 @@ } ], "badges": ["code_changes", "configuration_changes"] + }, + "luaToFunctions": { + "id": "luaToFunctions", + "title": "Consider using triggers and functions", + "tutorial": "/quick-guides/triggers-and-functions/introduction.md", + "content": [ + { + "type": "paragraph", + "value": "If you are using LUA scripts to run application logic inside Redis, consider using triggers and functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." + }, + { + "type": "spacer", + "value": "l" + }, + { + "type": "paragraph", + "value": "Triggers and functions can execute business logic on changes within a database, and read across all shards in clustered databases." + }, + { + "type": "spacer", + "value": "l" + }, + { + "type": "span", + "value": "These capabilities are part of " + }, + { + "type": "link", + "value": { + "href": "https://redis.io/docs/stack/about/", + "name": "Redis Stack" + } + }, + { + "type": "span", + "value": ", " + }, + { + "type": "link", + "value": { + "href": "https://redis.com/redis-enterprise-cloud/overview/", + "name": "Redis Enterprise Cloud" + } + }, + { + "type": "span", + "value": " and " + }, + { + "type": "link", + "value": { + "href": "https://redis.com/redis-enterprise-software/overview/", + "name": "Redis Enterprise Software" + } + }, + { + "type": "span", + "value": "." + } + ], + "badges": ["code_changes"] + }, + "functionsWithStreams": { + "id": "functionsWithStreams", + "title": "Consider using triggers and functions to react in real-time to stream entries", + "tutorial": "/quick-guides/triggers-and-functions/introduction.md", + "content": [ + { + "type": "paragraph", + "value": "If you need to manipulate your data based on Redis stream entries, consider using stream triggers that are a part of triggers and functions. It can help lower latency by moving business logic closer to the data." + }, + { + "type": "spacer", + "value": "l" + }, + { + "type": "paragraph", + "value": "Try triggers and functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." + }, + { + "type": "spacer", + "value": "l" + }, + { + "type": "paragraph", + "value": "These capabilities can execute business logic on changes within a database, and read across all shards in clustered databases." + }, + { + "type": "spacer", + "value": "l" + }, + { + "type": "span", + "value": "Triggers and functions are part of " + }, + { + "type": "link", + "value": { + "href": "https://redis.io/docs/stack/about/", + "name": "Redis Stack" + } + }, + { + "type": "span", + "value": ", " + }, + { + "type": "link", + "value": { + "href": "https://redis.com/redis-enterprise-cloud/overview/", + "name": "Redis Enterprise Cloud" + } + }, + { + "type": "span", + "value": " and " + }, + { + "type": "link", + "value": { + "href": "https://redis.com/redis-enterprise-software/overview/", + "name": "Redis Enterprise Software" + } + }, + { + "type": "span", + "value": "." + }, + { + "type": "spacer", + "value": "l" + }, + { + "type": "paragraph", + "value": "Try the interactive tutorial to learn more about triggers and functions." + } + ], + "badges": ["code_changes"] + }, + "functionsWithKeyspace": { + "id": "functionsWithKeyspace", + "title": "Consider using triggers and functions to react in real-time to database changes", + "tutorial": "/quick-guides/triggers-and-functions/introduction.md", + "content": [ + { + "type": "paragraph", + "value": "If you need to manipulate your data based on keyspace notifications, consider using keyspace triggers that are a part of triggers and functions. It can help lower latency by moving business logic closer to the data." + }, + { + "type": "spacer", + "value": "l" + }, + { + "type": "paragraph", + "value": "Try triggers and functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." + }, + { + "type": "spacer", + "value": "l" + }, + { + "type": "paragraph", + "value": "These capabilities can execute business logic on changes within a database, and read across all shards in clustered databases." + }, + { + "type": "spacer", + "value": "l" + }, + { + "type": "span", + "value": "Triggers and functions are part of " + }, + { + "type": "link", + "value": { + "href": "https://redis.io/docs/stack/about/", + "name": "Redis Stack" + } + }, + { + "type": "span", + "value": ", " + }, + { + "type": "link", + "value": { + "href": "https://redis.com/redis-enterprise-cloud/overview/", + "name": "Redis Enterprise Cloud" + } + }, + { + "type": "span", + "value": " and " + }, + { + "type": "link", + "value": { + "href": "https://redis.com/redis-enterprise-software/overview/", + "name": "Redis Enterprise Software" + } + }, + { + "type": "span", + "value": "." + }, + { + "type": "spacer", + "value": "l" + }, + { + "type": "paragraph", + "value": "Try the interactive tutorial to learn more about triggers and functions." + } + ], + "badges": ["code_changes"] } } From e309e7ea8aa761e8b80bb6b0d8fa340ed17f3125 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Fri, 7 Jul 2023 15:58:53 +0200 Subject: [PATCH 048/106] api tests refactoring --- tests/e2e/helpers/api/api-common.ts | 4 +- tests/e2e/helpers/api/api-database.ts | 514 ++++++++------ tests/e2e/helpers/api/api-keys.ts | 19 +- tests/e2e/helpers/database.ts | 645 +++++++++++------- .../pageObjects/my-redis-databases-page.ts | 8 +- .../a-first-start-form/autodiscovery.e2e.ts | 14 +- .../critical-path/browser/bulk-delete.e2e.ts | 16 +- .../critical-path/browser/bulk-upload.e2e.ts | 10 +- .../browser/consumer-group.e2e.ts | 10 +- .../critical-path/browser/context.e2e.ts | 21 +- .../browser/filtering-history.e2e.ts | 10 +- .../critical-path/browser/filtering.e2e.ts | 18 +- .../critical-path/browser/formatters.e2e.ts | 14 +- .../critical-path/browser/hash-field.e2e.ts | 10 +- .../critical-path/browser/json-key.e2e.ts | 10 +- .../browser/keylist-actions.e2e.ts | 15 +- .../critical-path/browser/large-data.e2e.ts | 10 +- .../critical-path/browser/list-key.e2e.ts | 14 +- .../critical-path/browser/scan-keys.e2e.ts | 15 +- .../browser/search-capabilities.e2e.ts | 34 +- .../critical-path/browser/set-key.e2e.ts | 10 +- .../browser/stream-key-entry-deletion.e2e.ts | 10 +- .../critical-path/browser/stream-key.e2e.ts | 10 +- .../browser/stream-pending-messages.e2e.ts | 10 +- .../critical-path/browser/zset-key.e2e.ts | 10 +- .../cli/cli-command-helper.e2e.ts | 10 +- .../critical-path/cli/cli-critical.e2e.ts | 17 +- .../cluster-details/cluster-details.e2e.ts | 14 +- .../database-overview/database-index.e2e.ts | 15 +- .../database-overview.e2e.ts | 28 +- .../database/clone-databases.e2e.ts | 40 +- .../database/connecting-to-the-db.e2e.ts | 14 +- .../database/export-databases.e2e.ts | 55 +- .../database/import-databases.e2e.ts | 46 +- .../database/logical-databases.e2e.ts | 7 +- .../critical-path/database/modules.e2e.ts | 18 +- .../enablement-area-autoupdate.e2e.ts | 18 +- .../promo-button-autoupdate.e2e.ts | 9 +- .../memory-efficiency.e2e.ts | 30 +- .../memory-efficiency/recommendations.e2e.ts | 32 +- .../memory-efficiency/top-keys-table.e2e.ts | 14 +- .../critical-path/monitor/monitor.e2e.ts | 17 +- .../monitor/save-commands.e2e.ts | 11 +- .../notifications/notification-center.e2e.ts | 7 +- .../pub-sub/subscribe-unsubscribe.e2e.ts | 20 +- .../critical-path/settings/settings.e2e.ts | 5 +- .../critical-path/slow-log/slow-log.e2e.ts | 11 +- .../critical-path/tree-view/delimiter.e2e.ts | 19 +- .../tree-view/tree-view-improvements.e2e.ts | 36 +- .../critical-path/tree-view/tree-view.e2e.ts | 18 +- .../workbench/autocomplete.e2e.ts | 10 +- .../workbench/command-results.e2e.ts | 12 +- .../critical-path/workbench/context.e2e.ts | 10 +- .../critical-path/workbench/cypher.e2e.ts | 10 +- .../workbench/default-scripts-area.e2e.ts | 10 +- .../workbench/index-schema.e2e.ts | 10 +- .../workbench/json-workbench.e2e.ts | 10 +- .../redisearch-module-not-available.e2e.ts | 10 +- .../workbench/scripting-area.e2e.ts | 10 +- .../tests/regression/browser/add-keys.e2e.ts | 15 +- .../regression/browser/consumer-group.e2e.ts | 15 +- .../tests/regression/browser/context.e2e.ts | 10 +- .../browser/filtering-iteratively.e2e.ts | 18 +- .../tests/regression/browser/filtering.e2e.ts | 24 +- .../regression/browser/format-switcher.e2e.ts | 16 +- .../browser/formatter-warning.e2e.ts | 10 +- .../regression/browser/full-screen.e2e.ts | 22 +- .../browser/handle-dbsize-permissions.e2e.ts | 15 +- .../regression/browser/hash-field.e2e.ts | 10 +- .../regression/browser/key-messages.e2e.ts | 10 +- .../browser/keys-all-databases.e2e.ts | 37 +- .../browser/large-key-details-values.e2e.ts | 10 +- .../regression/browser/last-refresh.e2e.ts | 10 +- .../tests/regression/browser/list-key.e2e.ts | 10 +- .../regression/browser/onboarding.e2e.ts | 9 +- .../regression/browser/resize-columns.e2e.ts | 12 +- .../tests/regression/browser/scan-keys.e2e.ts | 5 +- .../tests/regression/browser/set-key.e2e.ts | 10 +- .../regression/browser/stream-key.e2e.ts | 10 +- .../browser/stream-pending-messages.e2e.ts | 15 +- .../regression/browser/survey-link.e2e.ts | 11 +- .../regression/browser/ttl-format.e2e.ts | 12 +- .../regression/browser/upload-json-key.e2e.ts | 10 +- .../regression/cli/cli-command-helper.e2e.ts | 15 +- .../regression/cli/cli-logical-db.e2e.ts | 17 +- .../regression/cli/cli-promote-workbench.ts | 15 +- .../regression/cli/cli-re-cluster.e2e.ts | 28 +- tests/e2e/tests/regression/cli/cli.e2e.ts | 23 +- .../database-overview/database-info.e2e.ts | 10 +- .../database-overview-keys.e2e.ts | 16 +- .../database-overview.e2e.ts | 10 +- .../database-overview/overview.e2e.ts | 7 +- .../database/database-list-search.e2e.ts | 28 +- .../database/database-sorting.e2e.ts | 30 +- .../tests/regression/database/edit-db.e2e.ts | 24 +- .../tests/regression/database/github.e2e.ts | 12 +- .../database/logical-databases.e2e.ts | 10 +- .../regression/database/redisstack.e2e.ts | 13 +- .../regression/insights/feature-flag.e2e.ts | 25 +- .../insights/live-recommendations.e2e.ts | 37 +- .../tests/regression/monitor/monitor.e2e.ts | 20 +- .../regression/monitor/save-commands.e2e.ts | 16 +- .../regression/pub-sub/debug-mode.e2e.ts | 10 +- .../pub-sub/pub-sub-oss-cluster-7.ts | 23 +- .../regression/shortcuts/shortcuts.e2e.ts | 5 +- .../regression/tree-view/tree-view.e2e.ts | 14 +- .../regression/workbench/autocomplete.e2e.ts | 10 +- .../workbench/autoexecute-button.e2e.ts | 10 +- .../workbench/command-results.e2e.ts | 19 +- .../tests/regression/workbench/context.e2e.ts | 10 +- .../tests/regression/workbench/cypher.e2e.ts | 10 +- .../workbench/default-scripts-area.e2e.ts | 10 +- .../workbench/editor-cleanup.e2e.ts | 16 +- .../workbench/empty-command-history.e2e.ts | 10 +- .../regression/workbench/group-mode.e2e.ts | 11 +- .../workbench/history-of-results.e2e.ts | 12 +- .../workbench/import-tutorials.e2e.ts | 21 +- .../regression/workbench/raw-mode.e2e.ts | 20 +- .../workbench/redis-stack-commands.e2e.ts | 16 +- .../redisearch-module-not-available.e2e.ts | 14 +- .../workbench/scripting-area.e2e.ts | 16 +- .../workbench-non-auto-guides.e2e.ts | 14 +- .../workbench/workbench-pipeline.e2e.ts | 16 +- .../workbench/workbench-re-cluster.e2e.ts | 28 +- tests/e2e/tests/smoke/browser/add-keys.e2e.ts | 9 +- .../tests/smoke/browser/edit-key-name.e2e.ts | 9 +- .../tests/smoke/browser/edit-key-value.e2e.ts | 10 +- .../e2e/tests/smoke/browser/filtering.e2e.ts | 11 +- .../e2e/tests/smoke/browser/hash-field.e2e.ts | 9 +- tests/e2e/tests/smoke/browser/json-key.e2e.ts | 9 +- tests/e2e/tests/smoke/browser/list-key.e2e.ts | 9 +- .../browser/list-of-keys-verifications.e2e.ts | 11 +- tests/e2e/tests/smoke/browser/set-key.e2e.ts | 14 +- .../smoke/browser/set-ttl-for-key.e2e.ts | 9 +- .../smoke/browser/verify-key-details.e2e.ts | 9 +- .../smoke/browser/verify-keys-refresh.e2e.ts | 9 +- tests/e2e/tests/smoke/browser/zset-key.e2e.ts | 9 +- .../tests/smoke/cli/cli-command-helper.e2e.ts | 9 +- tests/e2e/tests/smoke/cli/cli.e2e.ts | 11 +- .../database/add-db-from-welcome-page.e2e.ts | 15 +- .../smoke/database/add-sentinel-db.e2e.ts | 7 +- .../smoke/database/add-standalone-db.e2e.ts | 15 +- .../smoke/database/autodiscover-db.e2e.ts | 22 +- .../database/connecting-to-the-db.e2e.ts | 11 +- .../tests/smoke/database/delete-the-db.e2e.ts | 10 +- tests/e2e/tests/smoke/database/edit-db.e2e.ts | 13 +- .../smoke/workbench/json-workbench.e2e.ts | 9 +- .../smoke/workbench/scripting-area.e2e.ts | 19 +- 148 files changed, 1863 insertions(+), 1466 deletions(-) diff --git a/tests/e2e/helpers/api/api-common.ts b/tests/e2e/helpers/api/api-common.ts index 7966022f6c..e6aaa55319 100644 --- a/tests/e2e/helpers/api/api-common.ts +++ b/tests/e2e/helpers/api/api-common.ts @@ -1,4 +1,4 @@ -import * as request from 'supertest'; +import request from 'supertest'; import { Common } from '../common'; import { Methods } from '../constants'; @@ -18,7 +18,7 @@ export async function sendRequest( body?: Record ): Promise { const windowId = Common.getWindowId(); - let requestEndpoint; + let requestEndpoint: any; if (method === Methods.post) { (requestEndpoint = request(endpoint) diff --git a/tests/e2e/helpers/api/api-database.ts b/tests/e2e/helpers/api/api-database.ts index 5017125ab7..d546d4db3f 100644 --- a/tests/e2e/helpers/api/api-database.ts +++ b/tests/e2e/helpers/api/api-database.ts @@ -1,253 +1,341 @@ import { t } from 'testcafe'; import { Chance } from 'chance'; import { asyncFilter, doAsyncStuff } from '../async-helper'; -import { AddNewDatabaseParameters, OSSClusterParameters, databaseParameters, SentinelParameters, ClusterNodes } from '../../pageObjects/components/myRedisDatabase/add-redis-database'; +import { + AddNewDatabaseParameters, + OSSClusterParameters, + databaseParameters, + SentinelParameters, + ClusterNodes, +} from '../../pageObjects/components/myRedisDatabase/add-redis-database'; import { Methods } from '../constants'; import { sendRequest } from './api-common'; const chance = new Chance(); -/** - * Add a new Standalone database through api using host and port - * @param databaseParameters The database parameters - */ -export async function addNewStandaloneDatabaseApi(databaseParameters: AddNewDatabaseParameters): Promise { - const uniqueId = chance.string({ length: 10 }); - const requestBody: { - name?: string, - host: string, - port: number, - username?: string, - password?: string, - tls?: boolean, - verifyServerCert?: boolean, - caCert?: { - name: string, - certificate?: string - }, - clientCert?: { - name: string, - certificate?: string, - key?: string - } - } = { - 'name': databaseParameters.databaseName, - 'host': databaseParameters.host, - 'port': Number(databaseParameters.port), - 'username': databaseParameters.databaseUsername, - 'password': databaseParameters.databasePassword - }; - - if (databaseParameters.caCert) { - requestBody.tls = true; - requestBody.verifyServerCert = false; - requestBody.caCert = { - 'name': `ca}-${uniqueId}`, - 'certificate': databaseParameters.caCert.certificate - }; - requestBody.clientCert = { - 'name': `client}-${uniqueId}`, - 'certificate': databaseParameters.clientCert!.certificate, - 'key': databaseParameters.clientCert!.key +export class DatabaseAPIRequests { + /** + * Add a new Standalone database through api using host and port + * @param databaseParameters The database parameters + */ + async addNewStandaloneDatabaseApi( + databaseParameters: AddNewDatabaseParameters + ): Promise { + const uniqueId = chance.string({ length: 10 }); + const requestBody: { + name?: string; + host: string; + port: number; + username?: string; + password?: string; + tls?: boolean; + verifyServerCert?: boolean; + caCert?: { + name: string; + certificate?: string; + }; + clientCert?: { + name: string; + certificate?: string; + key?: string; + }; + } = { + name: databaseParameters.databaseName, + host: databaseParameters.host, + port: Number(databaseParameters.port), + username: databaseParameters.databaseUsername, + password: databaseParameters.databasePassword, }; - } - const response = await sendRequest(Methods.post, '/databases', 201, requestBody); - await t.expect(await response.body.name).eql(databaseParameters.databaseName, `Database Name is not equal to ${databaseParameters.databaseName} in response`); -} -/** - * Add a new Standalone databases through api using host and port - * @param databasesParameters The databases parameters array - */ -export async function addNewStandaloneDatabasesApi(databasesParameters: AddNewDatabaseParameters[]): Promise { - if (databasesParameters.length) { - databasesParameters.forEach(async parameter => { - await addNewStandaloneDatabaseApi(parameter); - }); + if (databaseParameters.caCert) { + requestBody.tls = true; + requestBody.verifyServerCert = false; + requestBody.caCert = { + name: `ca}-${uniqueId}`, + certificate: databaseParameters.caCert.certificate, + }; + requestBody.clientCert = { + name: `client}-${uniqueId}`, + certificate: databaseParameters.clientCert!.certificate, + key: databaseParameters.clientCert!.key, + }; + } + const response = await sendRequest( + Methods.post, + '/databases', + 201, + requestBody + ); + await t + .expect(await response.body.name) + .eql( + databaseParameters.databaseName, + `Database Name is not equal to ${databaseParameters.databaseName} in response` + ); } -} -/** - * Add a new database from OSS Cluster through api using host and port - * @param databaseParameters The database parameters - */ -export async function addNewOSSClusterDatabaseApi(databaseParameters: OSSClusterParameters): Promise { - const requestBody = { - 'name': databaseParameters.ossClusterDatabaseName, - 'host': databaseParameters.ossClusterHost, - 'port': Number(databaseParameters.ossClusterPort) - }; - const response = await sendRequest(Methods.post, '/databases', 201, requestBody); - await t.expect(await response.body.name).eql(databaseParameters.ossClusterDatabaseName, `Database Name is not equal to ${databaseParameters.ossClusterDatabaseName} in response`); -} + /** + * Add a new Standalone databases through api using host and port + * @param databasesParameters The databases parameters array + */ + async addNewStandaloneDatabasesApi( + databasesParameters: AddNewDatabaseParameters[] + ): Promise { + if (databasesParameters.length) { + databasesParameters.forEach(async (parameter) => { + await this.addNewStandaloneDatabaseApi(parameter); + }); + } + } -/** - * Add a Sentinel database via autodiscover through api - * @param databaseParameters The database parameters - * @param primaryGroupsNumber Number of added primary groups - */ -export async function discoverSentinelDatabaseApi(databaseParameters: SentinelParameters, primaryGroupsNumber?: number): Promise { - let masters = databaseParameters.masters; - if (primaryGroupsNumber) { - masters = databaseParameters.masters!.slice(0, primaryGroupsNumber); + /** + * Add a new database from OSS Cluster through api using host and port + * @param databaseParameters The database parameters + */ + async addNewOSSClusterDatabaseApi( + databaseParameters: OSSClusterParameters + ): Promise { + const requestBody = { + name: databaseParameters.ossClusterDatabaseName, + host: databaseParameters.ossClusterHost, + port: Number(databaseParameters.ossClusterPort), + }; + const response = await sendRequest( + Methods.post, + '/databases', + 201, + requestBody + ); + await t + .expect(await response.body.name) + .eql( + databaseParameters.ossClusterDatabaseName, + `Database Name is not equal to ${databaseParameters.ossClusterDatabaseName} in response` + ); } - const requestBody = { - 'host': databaseParameters.sentinelHost, - 'port': Number(databaseParameters.sentinelPort), - 'password': databaseParameters.sentinelPassword, - 'masters': masters - }; - - await sendRequest(Methods.post, '/redis-sentinel/databases', 201, requestBody); -} -/** - * Get all databases through api - */ -export async function getAllDatabases(): Promise { - const response = await sendRequest(Methods.get, '/databases', 200); - return await response.body; -} + /** + * Add a Sentinel database via autodiscover through api + * @param databaseParameters The database parameters + * @param primaryGroupsNumber Number of added primary groups + */ + async discoverSentinelDatabaseApi( + databaseParameters: SentinelParameters, + primaryGroupsNumber?: number + ): Promise { + let masters = databaseParameters.masters; + if (primaryGroupsNumber) { + masters = databaseParameters.masters!.slice(0, primaryGroupsNumber); + } + const requestBody = { + host: databaseParameters.sentinelHost, + port: Number(databaseParameters.sentinelPort), + password: databaseParameters.sentinelPassword, + masters: masters, + }; -/** - * Get database through api using database name - * @param databaseName The database name - */ -export async function getDatabaseIdByName(databaseName?: string): Promise { - if (!databaseName) { - throw new Error('Error: Missing databaseName'); - } - let databaseId; - const allDataBases = await getAllDatabases(); - const response = await asyncFilter(allDataBases, async(item: databaseParameters) => { - await doAsyncStuff(); - return item.name === databaseName; - }); - - if (response.length !== 0) { - databaseId = await response[0].id; + await sendRequest( + Methods.post, + '/redis-sentinel/databases', + 201, + requestBody + ); } - return databaseId; -} -/** - * Get database through api using database connection type - * @param connectionType The database connection type - */ -export async function getDatabaseByConnectionType(connectionType?: string): Promise { - if (!connectionType) { - throw new Error('Error: Missing connectionType'); + /** + * Get all databases through api + */ + async getAllDatabases(): Promise { + const response = await sendRequest(Methods.get, '/databases', 200); + return await response.body; } - const allDataBases = await getAllDatabases(); - let response: object = {}; - response = await asyncFilter(allDataBases, async(item: databaseParameters) => { - await doAsyncStuff(); - return item.connectionType === connectionType; - }); - return await response[0].id; -} -/** - * Delete all databases through api - */ -export async function deleteAllDatabasesApi(): Promise { - const allDatabases = await getAllDatabases(); - if (allDatabases.length > 0) { - const databaseIds: string[] = []; - for (let i = 0; i < allDatabases.length; i++) { - const dbData = JSON.parse(JSON.stringify(allDatabases[i])); - databaseIds.push(dbData.id); + /** + * Get database through api using database name + * @param databaseName The database name + */ + async getDatabaseIdByName(databaseName?: string): Promise { + if (!databaseName) { + throw new Error('Error: Missing databaseName'); } - if (databaseIds.length > 0) { - const requestBody = { 'ids': databaseIds }; - await sendRequest(Methods.delete, '/databases', 200, requestBody); + let databaseId; + const allDataBases = await this.getAllDatabases(); + const response = await asyncFilter( + allDataBases, + async (item: databaseParameters) => { + await doAsyncStuff(); + return item.name === databaseName; + } + ); + + if (response.length !== 0) { + databaseId = await response[0].id; } + return databaseId; } -} -/** - * Delete Standalone database through api - * @param databaseParameters The database parameters - */ -export async function deleteStandaloneDatabaseApi(databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.databaseName); - if (databaseId) { - const requestBody = { 'ids': [`${databaseId}`] }; - await sendRequest(Methods.delete, '/databases', 200, requestBody); + /** + * Get database through api using database connection type + * @param connectionType The database connection type + */ + async getDatabaseByConnectionType( + connectionType?: string + ): Promise { + if (!connectionType) { + throw new Error('Error: Missing connectionType'); + } + const allDataBases = await this.getAllDatabases(); + let response: databaseParameters[] = []; + response = await asyncFilter( + allDataBases, + async (item: databaseParameters) => { + await doAsyncStuff(); + return item.connectionType === connectionType; + } + ); + return response[0].id; } - else { - throw new Error('Error: Missing databaseId'); + + /** + * Delete all databases through api + */ + async deleteAllDatabasesApi(): Promise { + const allDatabases = await this.getAllDatabases(); + if (allDatabases.length > 0) { + const databaseIds: string[] = []; + for (let i = 0; i < allDatabases.length; i++) { + const dbData = JSON.parse(JSON.stringify(allDatabases[i])); + databaseIds.push(dbData.id); + } + if (databaseIds.length > 0) { + const requestBody = { ids: databaseIds }; + await sendRequest( + Methods.delete, + '/databases', + 200, + requestBody + ); + } + } } -} -/** - * Delete Standalone databases using their names through api - * @param databaseNames Databases names - */ -export async function deleteStandaloneDatabasesByNamesApi(databaseNames: string[]): Promise { - databaseNames.forEach(async databaseName => { - const databaseId = await getDatabaseIdByName(databaseName); + /** + * Delete Standalone database through api + * @param databaseParameters The database parameters + */ + async deleteStandaloneDatabaseApi( + databaseParameters: AddNewDatabaseParameters + ): Promise { + const databaseId = await this.getDatabaseIdByName( + databaseParameters.databaseName + ); if (databaseId) { - const requestBody = { 'ids': [`${databaseId}`] }; + const requestBody = { ids: [`${databaseId}`] }; await sendRequest(Methods.delete, '/databases', 200, requestBody); - } - else { + } else { throw new Error('Error: Missing databaseId'); } - }); -} + } -/** - * Delete database from OSS Cluster through api - * @param databaseParameters The database parameters - */ -export async function deleteOSSClusterDatabaseApi(databaseParameters: OSSClusterParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.ossClusterDatabaseName); - const requestBody = { 'ids': [`${databaseId}`] }; - await sendRequest(Methods.delete, '/databases', 200, requestBody); -} + /** + * Delete Standalone databases using their names through api + * @param databaseNames Databases names + */ + async deleteStandaloneDatabasesByNamesApi( + databaseNames: string[] + ): Promise { + databaseNames.forEach(async (databaseName) => { + const databaseId = await this.getDatabaseIdByName(databaseName); + if (databaseId) { + const requestBody = { ids: [`${databaseId}`] }; + await sendRequest( + Methods.delete, + '/databases', + 200, + requestBody + ); + } else { + throw new Error('Error: Missing databaseId'); + } + }); + } -/** - * Delete all primary groups from Sentinel through api - * @param databaseParameters The database parameters - */ -export async function deleteAllSentinelDatabasesApi(databaseParameters: SentinelParameters): Promise { - for (let i = 0; i < databaseParameters.name!.length; i++) { - const databaseId = await getDatabaseIdByName(databaseParameters.name![i]); - const requestBody = { 'ids': [`${databaseId}`] }; + /** + * Delete database from OSS Cluster through api + * @param databaseParameters The database parameters + */ + async deleteOSSClusterDatabaseApi( + databaseParameters: OSSClusterParameters + ): Promise { + const databaseId = await this.getDatabaseIdByName( + databaseParameters.ossClusterDatabaseName + ); + const requestBody = { ids: [`${databaseId}`] }; await sendRequest(Methods.delete, '/databases', 200, requestBody); } -} -/** - * Delete all databases by connection type - */ -export async function deleteAllDatabasesByConnectionTypeApi(connectionType: string): Promise { - const databaseIds = await getDatabaseByConnectionType(connectionType); - const requestBody = { 'ids': [`${databaseIds}`] }; - await sendRequest(Methods.delete, '/databases', 200, requestBody); -} + /** + * Delete all primary groups from Sentinel through api + * @param databaseParameters The database parameters + */ + async deleteAllSentinelDatabasesApi( + databaseParameters: SentinelParameters + ): Promise { + for (let i = 0; i < databaseParameters.name!.length; i++) { + const databaseId = await this.getDatabaseIdByName( + databaseParameters.name![i] + ); + const requestBody = { ids: [`${databaseId}`] }; + await sendRequest(Methods.delete, '/databases', 200, requestBody); + } + } -/** - * Delete Standalone databases through api - * @param databasesParameters The databases parameters as array - */ -export async function deleteStandaloneDatabasesApi(databasesParameters: AddNewDatabaseParameters[]): Promise { - if (databasesParameters.length) { - databasesParameters.forEach(async parameter => { - await deleteStandaloneDatabaseApi(parameter); - }); + /** + * Delete all databases by connection type + */ + async deleteAllDatabasesByConnectionTypeApi( + connectionType: string + ): Promise { + const databaseIds = await this.getDatabaseByConnectionType( + connectionType + ); + const requestBody = { ids: [`${databaseIds}`] }; + await sendRequest(Methods.delete, '/databases', 200, requestBody); } -} -/** - * Get OSS Cluster nodes - * @param databaseParameters The database parameters - */ -export async function getClusterNodesApi(databaseParameters: OSSClusterParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.ossClusterDatabaseName); - const response = await sendRequest(Methods.get, `/databases/${databaseId}/cluster-details`, 200); - const nodes = await response.body.nodes; - const nodeNames = await nodes.map((node: ClusterNodes) => (`${node.host }:${ node.port}`)); - return nodeNames; + /** + * Delete Standalone databases through api + * @param databasesParameters The databases parameters as array + */ + async deleteStandaloneDatabasesApi( + databasesParameters: AddNewDatabaseParameters[] + ): Promise { + if (databasesParameters.length) { + databasesParameters.forEach(async (parameter) => { + await this.deleteStandaloneDatabaseApi(parameter); + }); + } + } + + /** + * Get OSS Cluster nodes + * @param databaseParameters The database parameters + */ + async getClusterNodesApi( + databaseParameters: OSSClusterParameters + ): Promise { + const databaseId = await this.getDatabaseIdByName( + databaseParameters.ossClusterDatabaseName + ); + const response = await sendRequest( + Methods.get, + `/databases/${databaseId}/cluster-details`, + 200 + ); + const nodes = await response.body.nodes; + const nodeNames = await nodes.map( + (node: ClusterNodes) => `${node.host}:${node.port}` + ); + return nodeNames; + } } diff --git a/tests/e2e/helpers/api/api-keys.ts b/tests/e2e/helpers/api/api-keys.ts index 69f7b4513e..8d42940b01 100644 --- a/tests/e2e/helpers/api/api-keys.ts +++ b/tests/e2e/helpers/api/api-keys.ts @@ -9,9 +9,10 @@ import { SortedSetKeyParameters, StreamKeyParameters } from '../../pageObjects/browser-page'; -import { getDatabaseIdByName } from './api-database'; +import { DatabaseAPIRequests } from './api-database'; const endpoint = Common.getEndpoint(); +const databaseAPIRequests = new DatabaseAPIRequests(); /** * Add Hash key @@ -19,7 +20,7 @@ const endpoint = Common.getEndpoint(); * @param databaseParameters The database parameters */ export async function addHashKeyApi(keyParameters: HashKeyParameters, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.databaseName); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); const response = await request(endpoint).post(`/databases/${databaseId}/hash?encoding=buffer`) .send({ 'keyName': keyParameters.keyName, @@ -36,7 +37,7 @@ export async function addHashKeyApi(keyParameters: HashKeyParameters, databasePa * @param databaseParameters The database parameters */ export async function addStreamKeyApi(keyParameters: StreamKeyParameters, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.databaseName); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); const response = await request(endpoint).post(`/databases/${databaseId}/streams?encoding=buffer`) .send({ 'keyName': keyParameters.keyName, @@ -53,7 +54,7 @@ export async function addStreamKeyApi(keyParameters: StreamKeyParameters, databa * @param databaseParameters The database parameters */ export async function addSetKeyApi(keyParameters: SetKeyParameters, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.databaseName); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); const response = await request(endpoint).post(`/databases/${databaseId}/set?encoding=buffer`) .send({ 'keyName': keyParameters.keyName, @@ -70,7 +71,7 @@ export async function addSetKeyApi(keyParameters: SetKeyParameters, databasePara * @param databaseParameters The database parameters */ export async function addSortedSetKeyApi(keyParameters: SortedSetKeyParameters, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.databaseName); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); const response = await request(endpoint).post(`/databases/${databaseId}/zSet?encoding=buffer`) .send({ 'keyName': keyParameters.keyName, @@ -87,7 +88,7 @@ export async function addSortedSetKeyApi(keyParameters: SortedSetKeyParameters, * @param databaseParameters The database parameters */ export async function addListKeyApi(keyParameters: ListKeyParameters, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.databaseName); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); const response = await request(endpoint).post(`/databases/${databaseId}/list?encoding=buffer`) .send({ 'keyName': keyParameters.keyName, @@ -104,7 +105,7 @@ export async function addListKeyApi(keyParameters: ListKeyParameters, databasePa * @param databaseParameters The database parameters */ export async function searchKeyByNameApi(keyName: string, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.databaseName); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); const response = await request(endpoint).get(`/databases/${databaseId}/keys?cursor=0&count=5000&match=${keyName}`) .set('Accept', 'application/json').expect(200); @@ -117,7 +118,7 @@ export async function searchKeyByNameApi(keyName: string, databaseParameters: Ad * @param databaseParameters The database parameters */ export async function deleteKeyByNameApi(keyName: string, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.databaseName); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); const isKeyExist = await searchKeyByNameApi(keyName, databaseParameters); if (isKeyExist.length > 0) { const response = await request(endpoint).delete(`/databases/${databaseId}/keys`) @@ -134,7 +135,7 @@ export async function deleteKeyByNameApi(keyName: string, databaseParameters: Ad * @param databaseParameters The database parameters */ export async function deleteKeysApi(keyNames: string[], databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await getDatabaseIdByName(databaseParameters.databaseName); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); const response = await request(endpoint).delete(`/databases/${databaseId}/keys`) .send({ 'keyNames': keyNames }) .set('Accept', 'application/json'); diff --git a/tests/e2e/helpers/database.ts b/tests/e2e/helpers/database.ts index 8057e040bf..6b0ed01a11 100644 --- a/tests/e2e/helpers/database.ts +++ b/tests/e2e/helpers/database.ts @@ -1,5 +1,9 @@ import { Selector, t } from 'testcafe'; -import { AddNewDatabaseParameters, SentinelParameters, OSSClusterParameters } from '../pageObjects/components/myRedisDatabase/add-redis-database'; +import { + AddNewDatabaseParameters, + SentinelParameters, + OSSClusterParameters, +} from '../pageObjects/components/myRedisDatabase/add-redis-database'; import { DiscoverMasterGroupsPage } from '../pageObjects/sentinel/discovered-sentinel-master-groups-page'; import { MyRedisDatabasePage, @@ -7,285 +11,430 @@ import { AutoDiscoverREDatabases, } from '../pageObjects'; import { UserAgreementDialog } from '../pageObjects/dialogs'; -import { addNewStandaloneDatabaseApi, discoverSentinelDatabaseApi, getDatabaseIdByName } from './api/api-database'; +import { DatabaseAPIRequests } from './api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const discoverMasterGroupsPage = new DiscoverMasterGroupsPage(); const autoDiscoverREDatabases = new AutoDiscoverREDatabases(); const browserPage = new BrowserPage(); const userAgreementDialog = new UserAgreementDialog(); +const databaseAPIRequests = new DatabaseAPIRequests(); -/** - * Add a new database manually using host and port - * @param databaseParameters The database parameters - */ -export async function addNewStandaloneDatabase(databaseParameters: AddNewDatabaseParameters): Promise { - // Fill the add database form - await myRedisDatabasePage.AddRedisDatabase.addRedisDataBase(databaseParameters); - // Click for saving - await t - .click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton) - // Wait for database to be exist - .expect(myRedisDatabasePage.dbNameList.withExactText(databaseParameters.databaseName ?? '').exists).ok('The database not displayed', { timeout: 10000 }) - // Close message - .click(myRedisDatabasePage.Toast.toastCloseButton); -} +export class DatabaseHelper { + /** + * Add a new database manually using host and port + * @param databaseParameters The database parameters + */ + async addNewStandaloneDatabase( + databaseParameters: AddNewDatabaseParameters + ): Promise { + // Fill the add database form + await myRedisDatabasePage.AddRedisDatabase.addRedisDataBase( + databaseParameters + ); + // Click for saving + await t + .click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton) + // Wait for database to be exist + .expect( + myRedisDatabasePage.dbNameList.withExactText( + databaseParameters.databaseName ?? '' + ).exists + ) + .ok('The database not displayed', { timeout: 10000 }) + // Close message + .click(myRedisDatabasePage.Toast.toastCloseButton); + } -/** - * Add a new database via autodiscover using Sentinel option - * @param databaseParameters The Sentinel parameters: host, port and sentinel password - */ -export async function discoverSentinelDatabase(databaseParameters: SentinelParameters): Promise { - // Fill sentinel parameters to auto-discover Master Groups - await myRedisDatabasePage.AddRedisDatabase.discoverSentinelDatabases(databaseParameters); - // Click for autodiscover - await t - .click(myRedisDatabasePage.AddRedisDatabase.discoverSentinelDatabaseButton) - .expect(discoverMasterGroupsPage.addPrimaryGroupButton.exists).ok('User is not on the second step of Sentinel flow', { timeout: 10000 }); - // Select Master Groups and Add to RedisInsight - await discoverMasterGroupsPage.addMasterGroups(); - await t.click(autoDiscoverREDatabases.viewDatabasesButton); -} + /** + * Add a new database via autodiscover using Sentinel option + * @param databaseParameters The Sentinel parameters: host, port and sentinel password + */ + async discoverSentinelDatabase( + databaseParameters: SentinelParameters + ): Promise { + // Fill sentinel parameters to auto-discover Master Groups + await myRedisDatabasePage.AddRedisDatabase.discoverSentinelDatabases( + databaseParameters + ); + // Click for autodiscover + await t + .click( + myRedisDatabasePage.AddRedisDatabase + .discoverSentinelDatabaseButton + ) + .expect(discoverMasterGroupsPage.addPrimaryGroupButton.exists) + .ok('User is not on the second step of Sentinel flow', { + timeout: 10000, + }); + // Select Master Groups and Add to RedisInsight + await discoverMasterGroupsPage.addMasterGroups(); + await t.click(autoDiscoverREDatabases.viewDatabasesButton); + } -/** - * Add a new database from RE Cluster via auto-discover flow - * @param databaseParameters The database parameters - */ -export async function addNewREClusterDatabase(databaseParameters: AddNewDatabaseParameters): Promise { - // Fill the add database form - await myRedisDatabasePage.AddRedisDatabase.addAutodiscoverREClusterDatabase(databaseParameters); - // Click on submit button - await t - .click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton) - // Wait for database to be exist in the list of Autodiscover databases and select it - .expect(autoDiscoverREDatabases.databaseName.withExactText(databaseParameters.databaseName ?? '').exists).ok('The database not displayed', { timeout: 10000 }) - .typeText(autoDiscoverREDatabases.search, databaseParameters.databaseName ?? '') - .click(autoDiscoverREDatabases.databaseCheckbox) - // Click Add selected databases button - .click(autoDiscoverREDatabases.addSelectedDatabases) - .click(autoDiscoverREDatabases.viewDatabasesButton); -} + /** + * Add a new database from RE Cluster via auto-discover flow + * @param databaseParameters The database parameters + */ + async addNewREClusterDatabase( + databaseParameters: AddNewDatabaseParameters + ): Promise { + // Fill the add database form + await myRedisDatabasePage.AddRedisDatabase.addAutodiscoverREClusterDatabase( + databaseParameters + ); + // Click on submit button + await t + .click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton) + // Wait for database to be exist in the list of Autodiscover databases and select it + .expect( + autoDiscoverREDatabases.databaseName.withExactText( + databaseParameters.databaseName ?? '' + ).exists + ) + .ok('The database not displayed', { timeout: 10000 }) + .typeText( + autoDiscoverREDatabases.search, + databaseParameters.databaseName ?? '' + ) + .click(autoDiscoverREDatabases.databaseCheckbox) + // Click Add selected databases button + .click(autoDiscoverREDatabases.addSelectedDatabases) + .click(autoDiscoverREDatabases.viewDatabasesButton); + } -/** - * Add a new database from OSS Cluster via auto-discover flow - * @param databaseParameters The database parameters - */ -export async function addOSSClusterDatabase(databaseParameters: OSSClusterParameters): Promise { - // Enter required parameters for OSS Cluster - await myRedisDatabasePage.AddRedisDatabase.addOssClusterDatabase(databaseParameters); - // Click for saving - await t - .click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton) - // Check for info message that DB was added - .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 }); -} + /** + * Add a new database from OSS Cluster via auto-discover flow + * @param databaseParameters The database parameters + */ + async addOSSClusterDatabase( + databaseParameters: OSSClusterParameters + ): Promise { + // Enter required parameters for OSS Cluster + await myRedisDatabasePage.AddRedisDatabase.addOssClusterDatabase( + databaseParameters + ); + // Click for saving + await t + .click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton) + // Check for info message that DB was added + .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 }); + } -/** - * Add a new database from Redis Enterprise Cloud via auto-discover flow - * @param cloudAPIAccessKey The Cloud API Access Key - * @param cloudAPISecretKey The Cloud API Secret Key - */ -export async function autodiscoverRECloudDatabase(cloudAPIAccessKey: string, cloudAPISecretKey: string): Promise { - // Fill the add database form and Submit - await myRedisDatabasePage.AddRedisDatabase.addAutodiscoverRECloudDatabase(cloudAPIAccessKey, cloudAPISecretKey); - await t.click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); - await t.expect(autoDiscoverREDatabases.title.withExactText('Redis Enterprise Cloud Subscriptions').exists).ok('Subscriptions list not displayed', { timeout: 120000 }); - // Select subscriptions - await t.click(myRedisDatabasePage.AddRedisDatabase.selectAllCheckbox); - await t.click(myRedisDatabasePage.AddRedisDatabase.showDatabasesButton); - // Select databases for adding - const databaseName = await autoDiscoverREDatabases.getDatabaseName(); - await t.click(autoDiscoverREDatabases.databaseCheckbox); - await t.click(autoDiscoverREDatabases.addSelectedDatabases); - // Wait for database to be exist in the My redis databases list - await t.expect(autoDiscoverREDatabases.title.withExactText('Redis Enterprise Databases Added').exists).ok('Added databases list not displayed', { timeout: 20000 }); - await t.click(autoDiscoverREDatabases.viewDatabasesButton); - // uncomment when fixed db will be added to cloud subscription - // await t.expect(myRedisDatabasePage.dbNameList.withExactText(databaseName).exists).ok('The database not displayed', { timeout: 10000 }); - return databaseName; -} + /** + * Add a new database from Redis Enterprise Cloud via auto-discover flow + * @param cloudAPIAccessKey The Cloud API Access Key + * @param cloudAPISecretKey The Cloud API Secret Key + */ + async autodiscoverRECloudDatabase( + cloudAPIAccessKey: string, + cloudAPISecretKey: string + ): Promise { + // Fill the add database form and Submit + await myRedisDatabasePage.AddRedisDatabase.addAutodiscoverRECloudDatabase( + cloudAPIAccessKey, + cloudAPISecretKey + ); + await t.click( + myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton + ); + await t + .expect( + autoDiscoverREDatabases.title.withExactText( + 'Redis Enterprise Cloud Subscriptions' + ).exists + ) + .ok('Subscriptions list not displayed', { timeout: 120000 }); + // Select subscriptions + await t.click(myRedisDatabasePage.AddRedisDatabase.selectAllCheckbox); + await t.click(myRedisDatabasePage.AddRedisDatabase.showDatabasesButton); + // Select databases for adding + const databaseName = await autoDiscoverREDatabases.getDatabaseName(); + await t.click(autoDiscoverREDatabases.databaseCheckbox); + await t.click(autoDiscoverREDatabases.addSelectedDatabases); + // Wait for database to be exist in the My redis databases list + await t + .expect( + autoDiscoverREDatabases.title.withExactText( + 'Redis Enterprise Databases Added' + ).exists + ) + .ok('Added databases list not displayed', { timeout: 20000 }); + await t.click(autoDiscoverREDatabases.viewDatabasesButton); + // uncomment when fixed db will be added to cloud subscription + // await t.expect(myRedisDatabasePage.dbNameList.withExactText(databaseName).exists).ok('The database not displayed', { timeout: 10000 }); + return databaseName; + } -/** - * Accept License terms and add database - * @param databaseParameters The database parameters - * @param databaseName The database name -*/ -export async function acceptLicenseTermsAndAddDatabase(databaseParameters: AddNewDatabaseParameters, databaseName: string): Promise { - await acceptLicenseTerms(); - await addNewStandaloneDatabase(databaseParameters); - // Connect to DB - await myRedisDatabasePage.clickOnDBByName(databaseName); -} + /** + * Accept License terms and add database + * @param databaseParameters The database parameters + * @param databaseName The database name + */ + async acceptLicenseTermsAndAddDatabase( + databaseParameters: AddNewDatabaseParameters + ): Promise { + await this.acceptLicenseTerms(); + await this.addNewStandaloneDatabase(databaseParameters); + // Connect to DB + await myRedisDatabasePage.clickOnDBByName( + databaseParameters.databaseName! + ); + } -/** - * Accept License terms and add database using api - * @param databaseParameters The database parameters - * @param databaseName The database name -*/ -export async function acceptLicenseTermsAndAddDatabaseApi(databaseParameters: AddNewDatabaseParameters, databaseName: string): Promise { - await acceptLicenseTerms(); - await addNewStandaloneDatabaseApi(databaseParameters); - // Reload Page to see the new added database through api - await myRedisDatabasePage.reloadPage(); - // Connect to DB - await myRedisDatabasePage.clickOnDBByName(databaseName); -} + /** + * Accept License terms and add database using api + * @param databaseParameters The database parameters + * @param databaseName The database name + */ + async acceptLicenseTermsAndAddDatabaseApi( + databaseParameters: AddNewDatabaseParameters + ): Promise { + await this.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi( + databaseParameters + ); + // Reload Page to see the new added database through api + await myRedisDatabasePage.reloadPage(); + // Connect to DB + await myRedisDatabasePage.clickOnDBByName( + databaseParameters.databaseName! + ); + } -/** - * Accept License terms and add OSS cluster database - * @param databaseParameters The database parameters - * @param databaseName The database name -*/ -export async function acceptLicenseTermsAndAddOSSClusterDatabase(databaseParameters: OSSClusterParameters, databaseName: string): Promise { - await acceptLicenseTerms(); - await addOSSClusterDatabase(databaseParameters); - // Connect to DB - await myRedisDatabasePage.clickOnDBByName(databaseName); -} + /** + * Accept License terms and add OSS cluster database + * @param databaseParameters The database parameters + * @param databaseName The database name + */ + async acceptLicenseTermsAndAddOSSClusterDatabase( + databaseParameters: OSSClusterParameters + ): Promise { + await this.acceptLicenseTerms(); + await this.addOSSClusterDatabase(databaseParameters); + // Connect to DB + await myRedisDatabasePage.clickOnDBByName( + databaseParameters.ossClusterDatabaseName! + ); + } -/** - * Accept License terms and add Sentinel database using api - * @param databaseParameters The database parameters -*/ -export async function acceptLicenseTermsAndAddSentinelDatabaseApi(databaseParameters: SentinelParameters): Promise { - await acceptLicenseTerms(); - await discoverSentinelDatabaseApi(databaseParameters); - // Reload Page to see the database added through api - await myRedisDatabasePage.reloadPage(); - // Connect to DB - await myRedisDatabasePage.clickOnDBByName(databaseParameters.masters![1].alias ?? ''); -} + /** + * Accept License terms and add Sentinel database using api + * @param databaseParameters The database parameters + */ + async acceptLicenseTermsAndAddSentinelDatabaseApi( + databaseParameters: SentinelParameters + ): Promise { + await this.acceptLicenseTerms(); + await databaseAPIRequests.discoverSentinelDatabaseApi( + databaseParameters + ); + // Reload Page to see the database added through api + await myRedisDatabasePage.reloadPage(); + // Connect to DB + await myRedisDatabasePage.clickOnDBByName( + databaseParameters.masters![1].alias ?? '' + ); + } -/** - * Accept License terms and add RE Cluster database - * @param databaseParameters The database parameters -*/ -export async function acceptLicenseTermsAndAddREClusterDatabase(databaseParameters: AddNewDatabaseParameters): Promise { - await acceptLicenseTerms(); - await addNewREClusterDatabase(databaseParameters); - // Connect to DB - await myRedisDatabasePage.clickOnDBByName(databaseParameters.databaseName ?? ''); -} + /** + * Accept License terms and add RE Cluster database + * @param databaseParameters The database parameters + */ + async acceptLicenseTermsAndAddREClusterDatabase( + databaseParameters: AddNewDatabaseParameters + ): Promise { + await this.acceptLicenseTerms(); + await this.addNewREClusterDatabase(databaseParameters); + // Connect to DB + await myRedisDatabasePage.clickOnDBByName( + databaseParameters.databaseName ?? '' + ); + } -/** - * Accept License terms and add RE Cloud database - * @param databaseParameters The database parameters -*/ -export async function acceptLicenseTermsAndAddRECloudDatabase(databaseParameters: AddNewDatabaseParameters): Promise { - const searchTimeout = 60 * 1000; // 60 sec to wait database appearing - const dbSelector = myRedisDatabasePage.dbNameList.withExactText(databaseParameters.databaseName ?? ''); - const startTime = Date.now(); + /** + * Accept License terms and add RE Cloud database + * @param databaseParameters The database parameters + */ + async acceptLicenseTermsAndAddRECloudDatabase( + databaseParameters: AddNewDatabaseParameters + ): Promise { + const searchTimeout = 60 * 1000; // 60 sec to wait database appearing + const dbSelector = myRedisDatabasePage.dbNameList.withExactText( + databaseParameters.databaseName ?? '' + ); + const startTime = Date.now(); - await acceptLicenseTerms(); - await myRedisDatabasePage.AddRedisDatabase.addRedisDataBase(databaseParameters); - // Click for saving - await t.click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); - await t.wait(3000); - // Reload page until db appears - do { - await myRedisDatabasePage.reloadPage(); + await this.acceptLicenseTerms(); + await myRedisDatabasePage.AddRedisDatabase.addRedisDataBase( + databaseParameters + ); + // Click for saving + await t.click( + myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton + ); + await t.wait(3000); + // Reload page until db appears + do { + await myRedisDatabasePage.reloadPage(); + } while ( + !(await dbSelector.exists) && + Date.now() - startTime < searchTimeout + ); + await t + .expect( + myRedisDatabasePage.dbNameList.withExactText( + databaseParameters.databaseName ?? '' + ).exists + ) + .ok('The database not displayed', { timeout: 5000 }); + await myRedisDatabasePage.clickOnDBByName( + databaseParameters.databaseName ?? '' + ); + await t + .expect(browserPage.keysSummary.exists) + .ok('Key list not loaded', { timeout: 15000 }); } - while (!(await dbSelector.exists) && Date.now() - startTime < searchTimeout); - await t.expect(myRedisDatabasePage.dbNameList.withExactText(databaseParameters.databaseName ?? '').exists).ok('The database not displayed', { timeout: 5000 }); - await myRedisDatabasePage.clickOnDBByName(databaseParameters.databaseName ?? ''); - await t.expect(browserPage.keysSummary.exists).ok('Key list not loaded', { timeout: 15000 }); -} -/** - * Add RE Cloud database - * @param databaseParameters The database parameters -*/ -export async function addRECloudDatabase(databaseParameters: AddNewDatabaseParameters): Promise { - const searchTimeout = 60 * 1000; // 60 sec to wait database appearing - const dbSelector = myRedisDatabasePage.dbNameList.withExactText(databaseParameters.databaseName ?? ''); - const startTime = Date.now(); + /** + * Add RE Cloud database + * @param databaseParameters The database parameters + */ + async addRECloudDatabase( + databaseParameters: AddNewDatabaseParameters + ): Promise { + const searchTimeout = 60 * 1000; // 60 sec to wait database appearing + const dbSelector = myRedisDatabasePage.dbNameList.withExactText( + databaseParameters.databaseName ?? '' + ); + const startTime = Date.now(); - await myRedisDatabasePage.AddRedisDatabase.addRedisDataBase(databaseParameters); - // Click for saving - await t.click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); - await t.wait(3000); - // Reload page until db appears - do { - await myRedisDatabasePage.reloadPage(); + await myRedisDatabasePage.AddRedisDatabase.addRedisDataBase( + databaseParameters + ); + // Click for saving + await t.click( + myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton + ); + await t.wait(3000); + // Reload page until db appears + do { + await myRedisDatabasePage.reloadPage(); + } while ( + !(await dbSelector.exists) && + Date.now() - startTime < searchTimeout + ); + await t + .expect( + myRedisDatabasePage.dbNameList.withExactText( + databaseParameters.databaseName ?? '' + ).exists + ) + .ok('The database not displayed', { timeout: 5000 }); } - while (!(await dbSelector.exists) && Date.now() - startTime < searchTimeout); - await t.expect(myRedisDatabasePage.dbNameList.withExactText(databaseParameters.databaseName ?? '').exists).ok('The database not displayed', { timeout: 5000 }); -} - -// Accept License terms -export async function acceptLicenseTerms(): Promise { - await t.maximizeWindow(); - await userAgreementDialog.acceptLicenseTerms(); -} -// Accept License terms and connect to the RedisStack database -export async function acceptLicenseAndConnectToRedisStack(): Promise { - await acceptLicenseTerms(); - //Connect to DB - await t - .click(myRedisDatabasePage.NavigationPanel.myRedisDBButton) - .click(myRedisDatabasePage.AddRedisDatabase.connectToRedisStackButton); -} + // Accept License terms + async acceptLicenseTerms(): Promise { + await t.maximizeWindow(); + await userAgreementDialog.acceptLicenseTerms(); + } -/** - * Delete database - * @param databaseName The database name -*/ -export async function deleteDatabase(databaseName: string): Promise { - await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); - if (await myRedisDatabasePage.AddRedisDatabase.addDatabaseButton.exists) { - await deleteDatabaseByNameApi(databaseName); + // Accept License terms and connect to the RedisStack database + async acceptLicenseAndConnectToRedisStack(): Promise { + await this.acceptLicenseTerms(); + //Connect to DB + await t + .click(myRedisDatabasePage.NavigationPanel.myRedisDBButton) + .click( + myRedisDatabasePage.AddRedisDatabase.connectToRedisStackButton + ); } -} -/** - * Delete database with custom name - * @param databaseName The database name -*/ -export async function deleteCustomDatabase(databaseName: string): Promise { - await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); - if (await myRedisDatabasePage.AddRedisDatabase.addDatabaseButton.exists) { - await myRedisDatabasePage.deleteDatabaseByName(databaseName); + /** + * Delete database + * @param databaseName The database name + */ + async deleteDatabase(databaseName: string): Promise { + await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); + if ( + await myRedisDatabasePage.AddRedisDatabase.addDatabaseButton.exists + ) { + await this.deleteDatabaseByNameApi(databaseName); + } } -} -/** - * Accept License terms and add database or connect to the Redis stask database - * @param databaseParameters The database parameters - * @param databaseName The database name -*/ -export async function acceptTermsAddDatabaseOrConnectToRedisStack(databaseParameters: AddNewDatabaseParameters, databaseName: string): Promise { - if (await myRedisDatabasePage.AddRedisDatabase.addDatabaseButton.exists) { - await acceptLicenseTermsAndAddDatabase(databaseParameters, databaseName); + /** + * Delete database with custom name + * @param databaseName The database name + */ + async deleteCustomDatabase(databaseName: string): Promise { + await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); + if ( + await myRedisDatabasePage.AddRedisDatabase.addDatabaseButton.exists + ) { + await myRedisDatabasePage.deleteDatabaseByName(databaseName); + } } - else { - await acceptLicenseAndConnectToRedisStack(); + + /** + * Accept License terms and add database or connect to the Redis stask database + * @param databaseParameters The database parameters + * @param databaseName The database name + */ + async acceptTermsAddDatabaseOrConnectToRedisStack( + databaseParameters: AddNewDatabaseParameters + ): Promise { + if ( + await myRedisDatabasePage.AddRedisDatabase.addDatabaseButton.exists + ) { + await this.acceptLicenseTermsAndAddDatabase(databaseParameters); + } else { + await this.acceptLicenseAndConnectToRedisStack(); + } } -} -/** - * Click on the edit database button by name - * @param databaseName The name of the database - */ -export async function clickOnEditDatabaseByName(databaseName: string): Promise { - const databaseId = await getDatabaseIdByName(databaseName); - const databaseEditBtn = Selector(`[data-testid=edit-instance-${databaseId}]`); + /** + * Click on the edit database button by name + * @param databaseName The name of the database + */ + async clickOnEditDatabaseByName(databaseName: string): Promise { + const databaseId = await databaseAPIRequests.getDatabaseIdByName( + databaseName + ); + const databaseEditBtn = Selector( + `[data-testid=edit-instance-${databaseId}]` + ); - await t.expect(databaseEditBtn.exists).ok(`"${databaseName}" database not displayed`); - await t.click(databaseEditBtn); -} + await t + .expect(databaseEditBtn.exists) + .ok(`"${databaseName}" database not displayed`); + await t.click(databaseEditBtn); + } -/** - * Delete database button by name - * @param databaseName The name of the database - */ -export async function deleteDatabaseByNameApi(databaseName: string): Promise { - const databaseId = await getDatabaseIdByName(databaseName); - const databaseDeleteBtn = Selector(`[data-testid=delete-instance-${databaseId}-icon]`); + /** + * Delete database button by name + * @param databaseName The name of the database + */ + async deleteDatabaseByNameApi(databaseName: string): Promise { + const databaseId = await databaseAPIRequests.getDatabaseIdByName( + databaseName + ); + const databaseDeleteBtn = Selector( + `[data-testid=delete-instance-${databaseId}-icon]` + ); - await t.expect(databaseDeleteBtn.exists).ok(`"${databaseName}" database not displayed`); - await t.click(databaseDeleteBtn); - await t.click(myRedisDatabasePage.confirmDeleteButton); + await t + .expect(databaseDeleteBtn.exists) + .ok(`"${databaseName}" database not displayed`); + await t.click(databaseDeleteBtn); + await t.click(myRedisDatabasePage.confirmDeleteButton); + } } diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index 434cfb9eea..984aba0750 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -1,8 +1,10 @@ import { t, Selector } from 'testcafe'; -import { getDatabaseIdByName } from '../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../helpers/api/api-database'; import { BasePage } from './base-page'; import { AddRedisDatabase } from './components/myRedisDatabase/add-redis-database'; +const databaseAPIRequests = new DatabaseAPIRequests(); + export class MyRedisDatabasePage extends BasePage { AddRedisDatabase = new AddRedisDatabase(); @@ -192,7 +194,7 @@ export class MyRedisDatabasePage extends BasePage { * @param databaseName The name of the database */ async verifyDatabaseStatusIsVisible(databaseName: string): Promise { - const databaseId = await getDatabaseIdByName(databaseName); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseName); const databaseEditBtn = Selector(`[data-testid=database-status-new-${databaseId}]`); await t.expect(databaseEditBtn.exists).ok(`Database status is not visible for ${databaseName}`); @@ -203,7 +205,7 @@ export class MyRedisDatabasePage extends BasePage { * @param databaseName The name of the database */ async verifyDatabaseStatusIsNotVisible(databaseName: string): Promise { - const databaseId = await getDatabaseIdByName(databaseName); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseName); const databaseEditBtn = Selector(`[data-testid=database-status-new-${databaseId}]`); await t.expect(databaseEditBtn.exists).notOk(`Database status is still visible for ${databaseName}`); diff --git a/tests/e2e/tests/critical-path/a-first-start-form/autodiscovery.e2e.ts b/tests/e2e/tests/critical-path/a-first-start-form/autodiscovery.e2e.ts index 6c13fcf002..fa0459f215 100644 --- a/tests/e2e/tests/critical-path/a-first-start-form/autodiscovery.e2e.ts +++ b/tests/e2e/tests/critical-path/a-first-start-form/autodiscovery.e2e.ts @@ -1,11 +1,11 @@ -import {MyRedisDatabasePage} from '../../../pageObjects'; -import { - commonUrl -} from '../../../helpers/conf'; -import {env, rte} from '../../../helpers/constants'; -import {acceptLicenseTerms} from '../../../helpers/database'; +import { MyRedisDatabasePage } from '../../../pageObjects'; +import { commonUrl } from '../../../helpers/conf'; +import { env, rte } from '../../../helpers/constants'; +import { DatabaseHelper } from '../../../helpers/database'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); + const standalonePorts = [8100, 8101, 8102, 8103, 12000]; const otherPorts = [28100, 8200]; @@ -13,7 +13,7 @@ fixture `Autodiscovery` .meta({ type: 'critical_path' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); test .meta({ env: env.desktop, rte: rte.none }) diff --git a/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts b/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts index a578e60974..8d6008d15a 100644 --- a/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts @@ -1,13 +1,15 @@ import { KeyTypesTexts, rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; import { deleteAllKeysFromDB, populateDBWithHashes } from '../../../helpers/keys'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const keyNames = [Common.generateWord(20), Common.generateWord(20)]; const dbParameters = { host: ossStandaloneRedisearch.host, port: ossStandaloneRedisearch.port }; @@ -18,7 +20,7 @@ fixture `Bulk Delete` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); await browserPage.addHashKey(keyNames[0], '100000', Common.generateWord(20), Common.generateWord(20)); await browserPage.addSetKey(keyNames[1], '100000', Common.generateWord(20)); if (await browserPage.Toast.toastCloseButton.exists) { @@ -28,7 +30,7 @@ fixture `Bulk Delete` .afterEach(async() => { // Clear and delete database await deleteAllKeysFromDB(dbParameters.host, dbParameters.port); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); test('Verify that user can access the bulk actions screen in the Browser', async t => { // Filter by Hash keys @@ -86,7 +88,7 @@ test('Verify that user can see blue progress line during the process of bulk del }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Add 1000000 Hash keys await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters2); await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters2); @@ -104,7 +106,7 @@ test }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Add 500000 keys await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters2); // Filter and search by Hash keys added @@ -135,7 +137,7 @@ test('Verify that when bulk deletion is completed, status Action completed is di }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); await browserPage.addSetKey(keyNames[1], '100000', Common.generateWord(20)); if (await browserPage.Toast.toastCloseButton.exists) { await t.click(browserPage.Toast.toastCloseButton); 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 b957165f55..b10eb089d4 100644 --- a/tests/e2e/tests/critical-path/browser/bulk-upload.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/bulk-upload.e2e.ts @@ -1,13 +1,15 @@ import * as path from 'path'; import { t } from 'testcafe'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { deleteAllKeysFromDB, verifyKeysDisplayedInTheList } from '../../../helpers/keys'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const dbParameters = { host: ossStandaloneRedisearch.host, port: ossStandaloneRedisearch.port }; const filesToUpload = ['bulkUplAllKeyTypes.txt', 'bigKeysData.rtf']; @@ -27,12 +29,12 @@ fixture `Bulk Upload` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); }) .afterEach(async() => { // Clear and delete database await deleteAllKeysFromDB(dbParameters.host, dbParameters.port); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); test('Verify bulk upload of different text docs formats', async t => { // Verify bulk upload for docker app version diff --git a/tests/e2e/tests/critical-path/browser/consumer-group.e2e.ts b/tests/e2e/tests/critical-path/browser/consumer-group.e2e.ts index d68a574e21..08f716ed62 100644 --- a/tests/e2e/tests/critical-path/browser/consumer-group.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/consumer-group.e2e.ts @@ -1,14 +1,16 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(20); let consumerGroupName = Common.generateWord(20); @@ -24,7 +26,7 @@ fixture `Consumer group` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async t => { // Clear and delete database @@ -32,7 +34,7 @@ fixture `Consumer group` await t.click(browserPage.closeKeyButton); } await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can create a new Consumer Group in the current Stream', async t => { const toolTip = [ diff --git a/tests/e2e/tests/critical-path/browser/context.e2e.ts b/tests/e2e/tests/critical-path/browser/context.e2e.ts index 548a0217ec..0dc7af682f 100644 --- a/tests/e2e/tests/critical-path/browser/context.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/context.e2e.ts @@ -1,16 +1,15 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { - MyRedisDatabasePage, - BrowserPage -} from '../../../pageObjects'; +import { DatabaseHelper } from '../../../helpers/database'; +import { MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; import { KeyTypesTexts, rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { verifySearchFilterValue } from '../../../helpers/keys'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const speed = 0.4; let keyName = Common.generateWord(10); @@ -20,11 +19,11 @@ fixture `Browser Context` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); // Update after resolving https://redislabs.atlassian.net/browse/RI-3299 test('Verify that user can see saved CLI size on Browser page when he returns back to Browser page', async t => { @@ -91,11 +90,11 @@ test('Verify that user can see saved executed commands in CLI on Browser page wh }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .after(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can see key details selected when he returns back to Browser page', async t => { // Scroll keys elements const scrollY = 1000; @@ -120,7 +119,7 @@ test .after(async() => { // Clear and delete database await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can see list of keys viewed on Browser page when he returns back to Browser page', async t => { const numberOfItems = 5000; const scrollY = 3200; diff --git a/tests/e2e/tests/critical-path/browser/filtering-history.e2e.ts b/tests/e2e/tests/critical-path/browser/filtering-history.e2e.ts index 1725a1dc10..fb201db2d9 100644 --- a/tests/e2e/tests/critical-path/browser/filtering-history.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/filtering-history.e2e.ts @@ -1,23 +1,25 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig } from '../../../helpers/conf'; import { KeyTypesTexts, rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Key name filters history` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test('Recent filters history', async t => { const keysForSearch = ['device', 'mobile']; diff --git a/tests/e2e/tests/critical-path/browser/filtering.e2e.ts b/tests/e2e/tests/critical-path/browser/filtering.e2e.ts index 4fd2432c1f..9cc7c859e0 100644 --- a/tests/e2e/tests/critical-path/browser/filtering.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/filtering.e2e.ts @@ -1,4 +1,4 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, @@ -7,10 +7,12 @@ import { } from '../../../helpers/conf'; import { keyLength, KeyTypesTexts, rte } from '../../../helpers/constants'; import { addKeysViaCli, deleteKeysViaCli, keyTypes } from '../../../helpers/keys'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keysData = keyTypes.map(object => ({ ...object })); @@ -20,17 +22,17 @@ fixture `Filtering per key name in Browser page` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can search a key with selected data type is filters', async t => { keyName = Common.generateWord(10); // Add new key @@ -58,7 +60,7 @@ test .after(async() => { // Clear keys and database await deleteKeysViaCli(keysData); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can filter keys per data type in Browser page', async t => { keyName = Common.generateWord(10); // Create new keys @@ -72,11 +74,11 @@ test }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .after(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user see the key type label when filtering per key types and when removes label the filter is removed on Browser page', async t => { //Check filtering labels for (const { textType } of keyTypes) { await browserPage.selectFilterGroupType(textType); diff --git a/tests/e2e/tests/critical-path/browser/formatters.e2e.ts b/tests/e2e/tests/critical-path/browser/formatters.e2e.ts index 6c2d413ede..fb98ad5cdc 100644 --- a/tests/e2e/tests/critical-path/browser/formatters.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/formatters.e2e.ts @@ -1,14 +1,16 @@ import { Selector } from 'testcafe'; import { keyLength, rte } from '../../../helpers/constants'; import { addKeysViaCli, deleteKeysViaCli, keyTypes } from '../../../helpers/keys'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; import { formatters, phpData } from '../../../test-data/formatters-data'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const keysData = keyTypes.map(object => ({ ...object })).filter((v, i) => i <= 6 && i !== 5); keysData.forEach(key => key.keyName = `${key.keyName}` + '-' + `${Common.generateWord(keyLength)}`); @@ -27,19 +29,19 @@ fixture `Formatters` }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Create new keys await addKeysViaCli(keysData); }) .afterEach(async() => { // Clear keys and database await deleteKeysViaCli(keysData); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); formattersHighlightedSet.forEach(formatter => { test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Create new keys await addKeysViaCli(keysData, formatter.fromText, formatter.fromText); })(`Verify that user can see highlighted key details in ${formatter.format} format`, async t => { @@ -124,7 +126,7 @@ formattersWithTooltipSet.forEach(formatter => { binaryFormattersSet.forEach(formatter => { test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Create new keys await addKeysViaCli(keysData, formatter.fromText); })(`Verify that user can see key details converted to ${formatter.format} format`, async t => { diff --git a/tests/e2e/tests/critical-path/browser/hash-field.e2e.ts b/tests/e2e/tests/critical-path/browser/hash-field.e2e.ts index 614adba5ec..b53e625f34 100644 --- a/tests/e2e/tests/critical-path/browser/hash-field.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/hash-field.e2e.ts @@ -1,11 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -16,12 +18,12 @@ fixture `Hash Key fields verification` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can search by full field name in Hash', async t => { keyName = Common.generateWord(10); 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 1a86f5b0d1..2f8cfb858c 100644 --- a/tests/e2e/tests/critical-path/browser/json-key.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/json-key.e2e.ts @@ -1,11 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -15,12 +17,12 @@ fixture `JSON Key verification` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can not add invalid JSON structure inside of created JSON', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/critical-path/browser/keylist-actions.e2e.ts b/tests/e2e/tests/critical-path/browser/keylist-actions.e2e.ts index 845bf2b084..f288ea7f6c 100644 --- a/tests/e2e/tests/critical-path/browser/keylist-actions.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/keylist-actions.e2e.ts @@ -1,14 +1,13 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName: string; @@ -16,12 +15,12 @@ fixture `Actions with Key List on Browser page` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); keyName = Common.generateWord(10); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can delete key in List mode', async t => { // Add new key diff --git a/tests/e2e/tests/critical-path/browser/large-data.e2e.ts b/tests/e2e/tests/critical-path/browser/large-data.e2e.ts index 4d11ac7c5e..267ccea23a 100644 --- a/tests/e2e/tests/critical-path/browser/large-data.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/large-data.e2e.ts @@ -1,11 +1,13 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { Common } from '../../../helpers/common'; import { rte } from '../../../helpers/constants'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); @@ -13,12 +15,12 @@ fixture `Cases with large data` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see relevant information about key size', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/critical-path/browser/list-key.e2e.ts b/tests/e2e/tests/critical-path/browser/list-key.e2e.ts index ce79edbd39..dd221249e2 100644 --- a/tests/e2e/tests/critical-path/browser/list-key.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/list-key.e2e.ts @@ -1,16 +1,18 @@ import { toNumber } from 'lodash'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -20,12 +22,12 @@ fixture `List Key verification` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can search List element by index', async t => { keyName = Common.generateWord(10); @@ -42,12 +44,12 @@ test('Verify that user can search List element by index', async t => { test .before(async() => { // add oss standalone v5 - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config, ossStandaloneV5Config.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config); }) .after(async() => { //Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); })('Verify that user can remove only one element for List for Redis v. <6.2', async t => { keyName = Common.generateWord(10); // Open CLI diff --git a/tests/e2e/tests/critical-path/browser/scan-keys.e2e.ts b/tests/e2e/tests/critical-path/browser/scan-keys.e2e.ts index c3bc98e0f8..31ad156e2c 100644 --- a/tests/e2e/tests/critical-path/browser/scan-keys.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/scan-keys.e2e.ts @@ -1,20 +1,19 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, BrowserPage, SettingsPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const settingsPage = new SettingsPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keys: string[] = []; @@ -31,12 +30,12 @@ fixture `Browser - Specify Keys to Scan` .page(commonUrl) .clientScripts({ content: `(${explicitErrorHandler.toString()})()` }) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { //Clear and delete database await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that the user can see this number of keys applied to new filter requests and to "scan more" functionality in Browser page', async t => { const searchPattern = 'key[12]*'; diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 2ba7956132..5f09b0a093 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -1,5 +1,5 @@ import { Selector, t } from 'testcafe'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, @@ -8,12 +8,14 @@ import { ossStandaloneV5Config } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList } from '../../../helpers/keys'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const patternModeTooltipText = 'Filter by Key Name or Pattern'; const redisearchModeTooltipText = 'Search by Values of Keys'; @@ -43,7 +45,7 @@ fixture `Search capabilities in Browser` .page(commonUrl); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); keyName = Common.generateWord(10); await browserPage.addHashKey(keyName); }) @@ -51,7 +53,7 @@ test // Clear and delete database await browserPage.deleteKeyByName(keyName); await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName}`]); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('RediSearch capabilities in Browser view to search per Hashes or JSONs', async t => { indexName = `idx:${keyName}`; keyNames = [`${keyName}:1`, `${keyName}:2`, `${keyName}:3`]; @@ -116,12 +118,12 @@ test }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .after(async() => { // Clear and delete database await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${indexName}`); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Search by index keys scanned for JSON', async t => { keyName = Common.generateWord(10); indexName = `idx:${keyName}`; @@ -143,10 +145,10 @@ test }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config, ossStandaloneV5Config.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config); }) .after(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); })('No RediSearch module message', async t => { const noRedisearchMessage = 'Looks like RediSearch is not available for this database'; // const externalPageLink = 'https://redis.com/try-free/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_browser_search'; @@ -162,11 +164,11 @@ test }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .after(async() => { await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${indexName}`); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Index creation', async t => { // const createIndexLink = 'https://redis.io/commands/ft.create/'; @@ -211,12 +213,12 @@ test }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { // Clear and delete database await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${indexName}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Context for RediSearch capability', async t => { keyName = Common.generateWord(10); indexName = `idx:${keyName}`; @@ -251,8 +253,8 @@ test test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, bigDbName); - await addNewStandaloneDatabaseApi(ossStandaloneConfig); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig); }) .after(async() => { //clear database @@ -265,8 +267,8 @@ test await browserPage.deleteKeysByNames(keyNames); //delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that indexed keys from previous DB are NOT displayed when user connects to another DB', async t => { /* Link to ticket: https://redislabs.atlassian.net/browse/RI-3863 diff --git a/tests/e2e/tests/critical-path/browser/set-key.e2e.ts b/tests/e2e/tests/critical-path/browser/set-key.e2e.ts index 245ef03b23..196f95b245 100644 --- a/tests/e2e/tests/critical-path/browser/set-key.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/set-key.e2e.ts @@ -1,11 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -15,12 +17,12 @@ fixture `Set Key fields verification` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can search by full member name in Set', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/critical-path/browser/stream-key-entry-deletion.e2e.ts b/tests/e2e/tests/critical-path/browser/stream-key-entry-deletion.e2e.ts index 425c735a4e..203918718c 100644 --- a/tests/e2e/tests/critical-path/browser/stream-key-entry-deletion.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/stream-key-entry-deletion.e2e.ts @@ -1,11 +1,13 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { rte } from '../../../helpers/constants'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(20); const fields = [ @@ -23,11 +25,11 @@ fixture `Stream key entry deletion` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that the Stream information is refreshed and the deleted entry is removed when user confirm the deletion of an entry', async t => { keyName = Common.generateWord(20); diff --git a/tests/e2e/tests/critical-path/browser/stream-key.e2e.ts b/tests/e2e/tests/critical-path/browser/stream-key.e2e.ts index b08cae778b..32b45b508d 100644 --- a/tests/e2e/tests/critical-path/browser/stream-key.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/stream-key.e2e.ts @@ -1,16 +1,18 @@ import { Chance } from 'chance'; import { Selector } from 'testcafe'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const chance = new Chance(); let keyName = Common.generateWord(20); @@ -21,12 +23,12 @@ fixture `Stream Key` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { //Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can create Stream key via Add New Key form', async t => { keyName = Common.generateWord(20); diff --git a/tests/e2e/tests/critical-path/browser/stream-pending-messages.e2e.ts b/tests/e2e/tests/critical-path/browser/stream-pending-messages.e2e.ts index 795adf8b23..11705a0f26 100644 --- a/tests/e2e/tests/critical-path/browser/stream-pending-messages.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/stream-pending-messages.e2e.ts @@ -1,14 +1,16 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(20); let consumerGroupName = Common.generateWord(20); @@ -17,7 +19,7 @@ fixture `Acknowledge and Claim of Pending messages` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async t => { // Clear and delete database @@ -25,7 +27,7 @@ fixture `Acknowledge and Claim of Pending messages` await t.click(browserPage.closeKeyButton); } await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can acknowledge any message in the list of pending messages', async t => { keyName = Common.generateWord(20); diff --git a/tests/e2e/tests/critical-path/browser/zset-key.e2e.ts b/tests/e2e/tests/critical-path/browser/zset-key.e2e.ts index 01efa33949..26c6175457 100644 --- a/tests/e2e/tests/critical-path/browser/zset-key.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/zset-key.e2e.ts @@ -1,11 +1,13 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { Common } from '../../../helpers/common'; import { rte } from '../../../helpers/constants'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -15,12 +17,12 @@ fixture `ZSet Key fields verification` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can search by member in Zset', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts b/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts index 316fbae06a..2b8b714176 100644 --- a/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts +++ b/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts @@ -1,10 +1,12 @@ import { env, rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { BrowserPage } from '../../../pageObjects'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const defaultHelperText = 'Enter any command in CLI or use search to see detailed information.'; const COMMAND_APPEND = 'APPEND'; @@ -16,11 +18,11 @@ fixture `CLI Command helper` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify Command Helper search and filter', async t => { // Open Command Helper diff --git a/tests/e2e/tests/critical-path/cli/cli-critical.e2e.ts b/tests/e2e/tests/critical-path/cli/cli-critical.e2e.ts index f2a13617a8..cec9a5011b 100644 --- a/tests/e2e/tests/critical-path/cli/cli-critical.e2e.ts +++ b/tests/e2e/tests/critical-path/cli/cli-critical.e2e.ts @@ -2,18 +2,17 @@ import { Chance } from 'chance'; import { Common } from '../../../helpers/common'; import { rte } from '../../../helpers/constants'; import { BrowserPage } from '../../../pageObjects'; -import { - acceptLicenseTermsAndAddDatabaseApi, - acceptLicenseTermsAndAddOSSClusterDatabase -} from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossClusterConfig, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteOSSClusterDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const chance = new Chance(); const pairsToSet = Common.createArrayPairsWithKeyValue(4); @@ -25,21 +24,21 @@ fixture `CLI critical` .meta({ type: 'critical_path' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .meta({ rte: rte.ossCluster }) .before(async() => { - await acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig, ossClusterConfig.ossClusterDatabaseName); + await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig); }) .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); })('Verify that user is redirected to another node when he works in CLI with OSS Cluster', async t => { keyName = Common.generateWord(10); // Open CLI diff --git a/tests/e2e/tests/critical-path/cluster-details/cluster-details.e2e.ts b/tests/e2e/tests/critical-path/cluster-details/cluster-details.e2e.ts index 4b6faf3d8c..2303f9d77e 100644 --- a/tests/e2e/tests/critical-path/cluster-details/cluster-details.e2e.ts +++ b/tests/e2e/tests/critical-path/cluster-details/cluster-details.e2e.ts @@ -1,15 +1,17 @@ import { Selector } from 'testcafe'; import { BrowserPage, MyRedisDatabasePage, ClusterDetailsPage, WorkbenchPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddOSSClusterDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossClusterConfig } from '../../../helpers/conf'; -import { deleteOSSClusterDatabaseApi, getClusterNodesApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const clusterDetailsPage = new ClusterDetailsPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const headerColumns = { 'Type': 'OSS Cluster', @@ -25,12 +27,12 @@ fixture `Overview` .meta({ type: 'critical_path', rte: rte.ossCluster }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig, ossClusterConfig.ossClusterDatabaseName); + await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig); // Go to Analysis Tools page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); }) .afterEach(async() => { - await deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); }); test('Overview tab header for OSS Cluster', async t => { const uptime = /[1-9][0-9]\s|[0-9]\smin|[1-9][0-9]\smin|[0-9]\sh/; @@ -50,11 +52,11 @@ test //Clear database and delete await browserPage.Cli.sendCommandInCli(`DEL ${keyName}`); await browserPage.Cli.sendCommandInCli('FT.DROPINDEX idx:schools DD'); - await deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); })('Primary node statistics table displaying', async t => { // Remember initial table values const initialValues: number[] = []; - const nodes = (await getClusterNodesApi(ossClusterConfig)).sort(); + const nodes = (await databaseAPIRequests.getClusterNodesApi(ossClusterConfig)).sort(); const columns = ['Commands/s', 'Clients', 'Total Keys', 'Network Input', 'Network Output', 'Total Memory']; for (const column in columns) { diff --git a/tests/e2e/tests/critical-path/database-overview/database-index.e2e.ts b/tests/e2e/tests/critical-path/database-overview/database-index.e2e.ts index 755f4ef13a..209efe1bc2 100644 --- a/tests/e2e/tests/critical-path/database-overview/database-index.e2e.ts +++ b/tests/e2e/tests/critical-path/database-overview/database-index.e2e.ts @@ -1,4 +1,4 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { KeyTypesTexts, rte } from '../../../helpers/constants'; import { Common } from '../../../helpers/common'; import { @@ -7,17 +7,16 @@ import { WorkbenchPage, MemoryEfficiencyPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList, verifySearchFilterValue } from '../../../helpers/keys'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const memoryEfficiencyPage = new MemoryEfficiencyPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const keyName = Common.generateWord(10); const indexName = `idx:${keyName}`; @@ -34,7 +33,7 @@ fixture `Allow to change database index` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Create 3 keys and index await browserPage.Cli.sendCommandsInCli(commands); }) @@ -45,7 +44,7 @@ fixture `Allow to change database index` // Delete and clear database await browserPage.OverviewPanel.changeDbIndex(0); await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `DEL ${keyName}`, `FT.DROPINDEX ${indexName}`]); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Switching between indexed databases', async t => { const command = `HSET ${logicalDbKey} "name" "Gillford School" "description" "Gillford School is a centre" "class" "private" "type" "democratic; waldorf" "address_city" "Goudhurst" "address_street" "Goudhurst" "students" 721 "location" "51.112685, 0.451076"`; diff --git a/tests/e2e/tests/critical-path/database-overview/database-overview.e2e.ts b/tests/e2e/tests/critical-path/database-overview/database-overview.e2e.ts index b1ce16f32c..b29b96f9cc 100644 --- a/tests/e2e/tests/critical-path/database-overview/database-overview.e2e.ts +++ b/tests/e2e/tests/critical-path/database-overview/database-overview.e2e.ts @@ -1,5 +1,5 @@ import { Chance } from 'chance'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { rte } from '../../../helpers/constants'; import { Common } from '../../../helpers/common'; import { @@ -13,12 +13,14 @@ import { ossStandaloneRedisearch, ossStandaloneBigConfig } from '../../../helpers/conf'; -import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const chance = new Chance(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const fiveSecondsTimeout = 5000; let keyName = chance.string({ length: 10 }); @@ -30,18 +32,18 @@ fixture `Database overview` .meta({ type: 'critical_path' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { //Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .meta({ rte: rte.standalone }) .after(async() => { //Delete databases - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Verify that user can see the list of Modules updated each time when he connects to the database', async t => { const firstDatabaseModules: string[] = []; const secondDatabaseModules: string[] = []; @@ -61,7 +63,7 @@ test } //Add database with different modules await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); - await addNewStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneRedisearch); await browserPage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneRedisearch.databaseName); countOfModules = await browserPage.modulesButton.count; @@ -102,8 +104,8 @@ test await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await browserPage.Cli.sendCommandInCli(`DEL ${keys1.join(' ')}`); await browserPage.Cli.sendCommandInCli(`DEL ${keys2.join(' ')}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can see total number of keys rounded in format 100, 1K, 1M, 1B in DB header in Browser page', async t => { //Add 100 keys keys1 = await Common.createArrayWithKeyValue(100); @@ -119,7 +121,7 @@ test await t.expect(totalKeys).eql('1 K', 'Info in DB header after ADD 1000 keys'); //Add database with more than 1M keys await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); - await addNewStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneBigConfig); await browserPage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneBigConfig.databaseName); //Wait 5 seconds @@ -133,7 +135,7 @@ test .after(async() => { //Clear and delete database await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can see total memory rounded in format B, KB, MB, GB, TB in DB header in Browser page', async t => { //Add new keys keys = await Common.createArrayWithKeyValue(100); @@ -145,14 +147,14 @@ test test .meta({ rte: rte.standalone }) .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); //Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .after(async() => { //Delete database and index await workbenchPage.sendCommandInWorkbench('FT.DROPINDEX idx:schools DD'); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can see additional information in Overview: Connected Clients, Commands/Sec, CPU (%) using Standalone DB connection type', async t => { const commandsSecBeforeEdit = await browserPage.overviewCommandsSec.textContent; //Wait 5 second 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 bb8ea4c45c..51cec4c848 100644 --- a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts @@ -1,17 +1,13 @@ import { rte } from '../../../helpers/constants'; import { MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossClusterConfig, ossSentinelConfig, ossStandaloneConfig } from '../../../helpers/conf'; -import { acceptLicenseTerms, clickOnEditDatabaseByName } from '../../../helpers/database'; -import { - addNewOSSClusterDatabaseApi, - addNewStandaloneDatabaseApi, - deleteAllDatabasesByConnectionTypeApi, - deleteOSSClusterDatabaseApi, - deleteStandaloneDatabaseApi, - discoverSentinelDatabaseApi -} from '../../../helpers/api/api-database'; +import { DatabaseHelper } from '../../../helpers/database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const newOssDatabaseAlias = 'cloned oss cluster'; fixture `Clone databases` @@ -19,19 +15,19 @@ fixture `Clone databases` .page(commonUrl); test .before(async() => { - await acceptLicenseTerms(); - await addNewStandaloneDatabaseApi(ossStandaloneConfig); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig); await myRedisDatabasePage.reloadPage(); }) .after(async() => { // Delete databases const dbNumber = await myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).count; for (let i = 0; i < dbNumber; i++) { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); } }) .meta({ rte: rte.standalone })('Verify that user can clone Standalone db', async t => { - await clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); + await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); // Verify that user can test Standalone connection on edit and see the success message await t.click(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); @@ -41,7 +37,7 @@ test await t.click(myRedisDatabasePage.AddRedisDatabase.cloneDatabaseButton); await t.click(myRedisDatabasePage.AddRedisDatabase.cancelButton); await t.expect(myRedisDatabasePage.editAliasButton.withText('Clone ').exists).notOk('Clone panel is still displayed', { timeout: 2000 }); - await clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); + await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); await t.click(myRedisDatabasePage.AddRedisDatabase.cloneDatabaseButton); // Verify that user see the “Add Database Manually” form pre-populated with all the connection data when cloning DB await t @@ -61,17 +57,17 @@ test }); test .before(async() => { - await acceptLicenseTerms(); - await addNewOSSClusterDatabaseApi(ossClusterConfig); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig); await myRedisDatabasePage.reloadPage(); }) .after(async() => { // Delete database - await deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); await myRedisDatabasePage.deleteDatabaseByName(newOssDatabaseAlias); }) .meta({ rte: rte.ossCluster })('Verify that user can clone OSS Cluster', async t => { - await clickOnEditDatabaseByName(ossClusterConfig.ossClusterDatabaseName); + await databaseHelper.clickOnEditDatabaseByName(ossClusterConfig.ossClusterDatabaseName); // Verify that user can test OSS Cluster connection on edit and see the success message await t.click(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); @@ -93,18 +89,18 @@ test }); test .before(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); // Add Sentinel databases - await discoverSentinelDatabaseApi(ossSentinelConfig); + await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig); await myRedisDatabasePage.reloadPage(); }) .after(async() => { // Delete all primary groups - await deleteAllDatabasesByConnectionTypeApi('SENTINEL'); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL'); await myRedisDatabasePage.reloadPage(); }) .meta({ rte: rte.sentinel })('Verify that user can clone Sentinel', async t => { - await clickOnEditDatabaseByName(ossSentinelConfig.masters[1].alias); + await databaseHelper.clickOnEditDatabaseByName(ossSentinelConfig.masters[1].alias); await t.click(myRedisDatabasePage.AddRedisDatabase.cloneDatabaseButton); // Verify that user can test Sentinel connection on edit and see the success message 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 53207e4085..12344d26a0 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 @@ -1,14 +1,16 @@ import { rte } from '../../../helpers/constants'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, invalidOssStandaloneConfig, ossStandaloneForSSHConfig } from '../../../helpers/conf'; -import { acceptLicenseTerms, clickOnEditDatabaseByName } from '../../../helpers/database'; -import { deleteStandaloneDatabasesByNamesApi } from '../../../helpers/api/api-database'; +import { DatabaseHelper } from '../../../helpers/database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { sshPrivateKey, sshPrivateKeyWithPasscode } from '../../../test-data/sshPrivateKeys'; import { Common } from '../../../helpers/common'; // import { BrowserActions } from '../../../common-actions/browser-actions'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); // const browserActions = new BrowserActions(); const sshParams = { @@ -34,7 +36,7 @@ fixture `Connecting to the databases verifications` .meta({ type: 'critical_path' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); test .meta({ rte: rte.none })('Verify that user can see error message if he can not connect to added Database', async t => { @@ -79,7 +81,7 @@ test .meta({ rte: rte.standalone }) .after(async() => { // Delete databases - await deleteStandaloneDatabasesByNamesApi([sshDbPass.databaseName, sshDbPrivateKey.databaseName, sshDbPasscode.databaseName, newClonedDatabaseAlias]); + await databaseAPIRequests.deleteStandaloneDatabasesByNamesApi([sshDbPass.databaseName, sshDbPrivateKey.databaseName, sshDbPasscode.databaseName, newClonedDatabaseAlias]); })('Adding database with SSH', async t => { // const tooltipText = [ // 'Enter a value for required fields (3):', @@ -137,13 +139,13 @@ test .typeText(myRedisDatabasePage.AddRedisDatabase.sshPassphraseInput, sshWithPassphrase.sshPassphrase, { replace: true, paste: true }); await t.click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); await t.expect(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton.exists).notOk('Edit database panel still displayed'); - await clickOnEditDatabaseByName(sshDbPrivateKey.databaseName); + await databaseHelper.clickOnEditDatabaseByName(sshDbPrivateKey.databaseName); await t .expect(myRedisDatabasePage.AddRedisDatabase.sshPrivateKeyInput.value).eql(sshWithPassphrase.sshPrivateKey, 'Edited Private key not saved') .expect(myRedisDatabasePage.AddRedisDatabase.sshPassphraseInput.value).eql(sshWithPassphrase.sshPassphrase, 'Edited Passphrase not saved'); // Verify that user can clone database with SSH tunnel - await clickOnEditDatabaseByName(sshDbPrivateKey.databaseName); + await databaseHelper.clickOnEditDatabaseByName(sshDbPrivateKey.databaseName); await t.click(myRedisDatabasePage.AddRedisDatabase.cloneDatabaseButton); // Edit Database alias before cloning await t.typeText(myRedisDatabasePage.AddRedisDatabase.databaseAliasInput, newClonedDatabaseAlias, { replace: true }); diff --git a/tests/e2e/tests/critical-path/database/export-databases.e2e.ts b/tests/e2e/tests/critical-path/database/export-databases.e2e.ts index f4ae1c8951..e197c07418 100644 --- a/tests/e2e/tests/critical-path/database/export-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/export-databases.e2e.ts @@ -11,19 +11,14 @@ import { ossStandaloneConfig, ossStandaloneTlsConfig } from '../../../helpers/conf'; -import { acceptLicenseTerms, addRECloudDatabase, clickOnEditDatabaseByName, deleteDatabase } from '../../../helpers/database'; -import { - addNewOSSClusterDatabaseApi, - addNewStandaloneDatabaseApi, - deleteAllDatabasesByConnectionTypeApi, - deleteOSSClusterDatabaseApi, - deleteStandaloneDatabaseApi, - discoverSentinelDatabaseApi -} from '../../../helpers/api/api-database'; +import { DatabaseHelper } from '../../../helpers/database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { DatabasesActions } from '../../../common-actions/databases-actions'; const myRedisDatabasePage = new MyRedisDatabasePage(); const databasesActions = new DatabasesActions(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let foundExportedFiles: string[]; @@ -32,19 +27,19 @@ fixture `Export databases` .page(commonUrl); test .before(async() => { - await acceptLicenseTerms(); - await addNewStandaloneDatabaseApi(ossStandaloneConfig); - await addNewStandaloneDatabaseApi(ossStandaloneTlsConfig); - await addNewOSSClusterDatabaseApi(ossClusterConfig); - await discoverSentinelDatabaseApi(ossSentinelConfig); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneTlsConfig); + await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig); await myRedisDatabasePage.reloadPage(); }) .after(async() => { // Delete all databases - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - await deleteStandaloneDatabaseApi(ossStandaloneTlsConfig); - await deleteOSSClusterDatabaseApi(ossClusterConfig); - await deleteAllDatabasesByConnectionTypeApi('SENTINEL'); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneTlsConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL'); // Delete exported file fs.unlinkSync(joinPath(fileDownloadPath, foundExportedFiles[0])); })('Exporting Standalone, OSS Cluster, and Sentinel connection types', async t => { @@ -69,10 +64,10 @@ test await t.expect(foundExportedFiles.length).gt(0, 'The Exported file not saved'); // Delete databases - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - await deleteStandaloneDatabaseApi(ossStandaloneTlsConfig); - await deleteOSSClusterDatabaseApi(ossClusterConfig); - await deleteAllDatabasesByConnectionTypeApi('SENTINEL'); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneTlsConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL'); await myRedisDatabasePage.reloadPage(); const exportedData = { @@ -87,23 +82,23 @@ test await t.click(myRedisDatabasePage.okDialogBtn); // Verify that user can import exported file with all datatypes and certificates await databasesActions.verifyDatabasesDisplayed(exportedData.dbImportedNames); - await clickOnEditDatabaseByName(databaseNames[1]); + await databaseHelper.clickOnEditDatabaseByName(databaseNames[1]); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).contains('ca', 'CA certificate import incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.textContent).contains('client', 'Client certificate import incorrect'); }); test .before(async() => { - await acceptLicenseTerms(); - await addNewStandaloneDatabaseApi(ossStandaloneTlsConfig); - await addRECloudDatabase(cloudDatabaseConfig); - await discoverSentinelDatabaseApi(ossSentinelConfig); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneTlsConfig); + await databaseHelper.addRECloudDatabase(cloudDatabaseConfig); + await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig); await myRedisDatabasePage.reloadPage(); }) .after(async() => { // Delete databases - await deleteStandaloneDatabaseApi(ossStandaloneTlsConfig); - await deleteDatabase(cloudDatabaseConfig.databaseName); - await deleteAllDatabasesByConnectionTypeApi('SENTINEL'); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneTlsConfig); + await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL'); // Delete exported file fs.unlinkSync(joinPath(fileDownloadPath, foundExportedFiles[0])); })('Export databases without passwords', async t => { diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index 1adc521f85..69f5e915c0 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -2,14 +2,16 @@ import * as path from 'path'; import { rte } from '../../../helpers/constants'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl } from '../../../helpers/conf'; -import { acceptLicenseTerms, clickOnEditDatabaseByName } from '../../../helpers/database'; -import { deleteStandaloneDatabasesByNamesApi } from '../../../helpers/api/api-database'; +import { DatabaseHelper } from '../../../helpers/database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { DatabasesActions } from '../../../common-actions/databases-actions'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const databasesActions = new DatabasesActions(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const fileNames = { racompassValidJson: 'racompass-valid.json', @@ -85,7 +87,7 @@ fixture `Import databases` .meta({ type: 'critical_path', rte: rte.none }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); test('Connection import modal window', async t => { const tooltipText = 'Import Database Connections'; @@ -125,7 +127,7 @@ test('Connection import modal window', async t => { test .after(async() => { // Delete databases - await deleteStandaloneDatabasesByNamesApi([...rdmData.dbImportedNames, ...databasesToDelete]); + await databaseAPIRequests.deleteStandaloneDatabasesByNamesApi([...rdmData.dbImportedNames, ...databasesToDelete]); })('Connection import from JSON', async t => { // Verify that user can import database with mandatory/optional fields await databasesActions.importDatabase(rdmData); @@ -144,7 +146,7 @@ test await t.click(myRedisDatabasePage.okDialogBtn); await databasesActions.verifyDatabasesDisplayed(rdmData.dbImportedNames); - await clickOnEditDatabaseByName(rdmData.dbImportedNames[1]); + await databaseHelper.clickOnEditDatabaseByName(rdmData.dbImportedNames[1]); // Verify username imported await t.expect(myRedisDatabasePage.AddRedisDatabase.usernameInput.value).eql(rdmListOfDB[1].username, 'Username import incorrect'); // Verify password imported @@ -152,7 +154,7 @@ test await t.expect(myRedisDatabasePage.AddRedisDatabase.passwordInput.value).eql(rdmListOfDB[1].auth, 'Password import incorrect'); // Verify cluster connection type imported - await clickOnEditDatabaseByName(rdmData.dbImportedNames[2]); + await databaseHelper.clickOnEditDatabaseByName(rdmData.dbImportedNames[2]); await t.expect(myRedisDatabasePage.AddRedisDatabase.connectionType.textContent).eql(rdmData.connectionType, 'Connection type import incorrect'); /* @@ -160,25 +162,25 @@ test Verify that user can import database with certificates by an absolute folder path(CA certificate, Client certificate, Client private key) Verify that user can see the certificate name as the certificate file name */ - await clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+CaCert')); + await databaseHelper.clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+CaCert')); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql('ca', 'CA certificate import incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.exists).notOk('Client certificate was imported'); // Verify that user can import database with Client certificate, Client private key - await clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+clientCert+privateKey')); + await databaseHelper.clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+clientCert+privateKey')); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.textContent).eql('client', 'Client certificate import incorrect'); // Verify that user can import database with all certificates - await clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+CaCert+clientCert+privateKey')); + await databaseHelper.clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+CaCert+clientCert+privateKey')); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql('ca', 'CA certificate import incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.textContent).eql('client', 'Client certificate import incorrect'); // Verify that certificate not imported when any certificate field has not been parsed - await clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmCaCertInvalidBody')); + await databaseHelper.clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmCaCertInvalidBody')); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.exists).notOk('Client certificate was imported'); - await clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmInvalidClientCert')); + await databaseHelper.clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmInvalidClientCert')); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.exists).notOk('Client certificate was imported'); @@ -190,7 +192,7 @@ test } // Verify that user can import Sentinel database connections by corresponding fields in JSON - await clickOnEditDatabaseByName(dbData[1].dbNames[2]); + await databaseHelper.clickOnEditDatabaseByName(dbData[1].dbNames[2]); await t.expect(myRedisDatabasePage.AddRedisDatabase.sentinelForm.textContent).contains('Sentinel', 'Sentinel connection type import incorrect'); await myRedisDatabasePage.clickOnDBByName(dbData[1].dbNames[2]); await Common.checkURLContainsText('browser'); @@ -198,43 +200,43 @@ test test .after(async() => { // Delete databases - await deleteStandaloneDatabasesByNamesApi(rdmCertsNames); + await databaseAPIRequests.deleteStandaloneDatabasesByNamesApi(rdmCertsNames); })('Certificates import with/without path', async t => { await databasesActions.importDatabase({ path: rdmData.sshPath }); await t.click(myRedisDatabasePage.okDialogBtn); // Verify that when user imports a certificate and the same certificate body already exists, the existing certificate (with its name) is applied - await clickOnEditDatabaseByName(rdmListOfCertsDB[0].name); + await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[0].name); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql(rdmListOfCertsDB[0].caCert.name, 'CA certificate import incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.textContent).eql(rdmListOfCertsDB[0].clientCert.name, 'Client certificate import incorrect'); - await clickOnEditDatabaseByName(rdmListOfCertsDB[1].name); + await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[1].name); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql(rdmListOfCertsDB[0].caCert.name, 'CA certificate name with the same body is incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.textContent).eql(rdmListOfCertsDB[0].clientCert.name, 'Client certificate name with the same body is incorrect'); // Verify that when user imports a certificate and the same certificate name exists but with a different body, the certificate imported with "({incremental_number})_certificate_name" name - await clickOnEditDatabaseByName(rdmListOfCertsDB[2].name); + await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[2].name); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql(`1_${rdmListOfCertsDB[0].caCert.name}`, 'CA certificate name with the same body is incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.textContent).eql(`1_${rdmListOfCertsDB[0].clientCert.name}`, 'Client certificate name with the same body is incorrect'); // Verify that when user imports a certificate by path and the same certificate body already exists, the existing certificate (with its name) is applied - await clickOnEditDatabaseByName(rdmListOfCertsDB[3].name); + await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[3].name); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql('caPath', 'CA certificate import incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.textContent).eql('clientPath', 'Client certificate import incorrect'); - await clickOnEditDatabaseByName(rdmListOfCertsDB[4].name); + await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[4].name); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql('caPath', 'CA certificate import incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.textContent).eql('clientPath', 'Client certificate import incorrect'); // Verify that when user imports a certificate by path and the same certificate name exists but with a different body, the certificate imported with "({incremental_number})certificate_name" name - await clickOnEditDatabaseByName(rdmListOfCertsDB[5].name); + await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[5].name); await t.expect(myRedisDatabasePage.AddRedisDatabase.caCertField.textContent).eql('1_caPath', 'CA certificate import incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.clientCertField.textContent).eql('1_clientPath', 'Client certificate import incorrect'); }); test .after(async() => { // Delete databases - await deleteStandaloneDatabasesByNamesApi(racompSSHData.importedSSHdbNames); + await databaseAPIRequests.deleteStandaloneDatabasesByNamesApi(racompSSHData.importedSSHdbNames); })('Import SSH parameters', async t => { const sshAgentsResult = 'SSH Agents are not supported'; const sshPrivateKey = '-----BEGIN OPENSSH PRIVATE KEY-----'; @@ -255,14 +257,14 @@ test await t.expect(myRedisDatabasePage.importResult.withText(sshAgentsResult).exists).ok('SSH agents not supported message not displayed in result'); await t.click(myRedisDatabasePage.okDialogBtn); - await clickOnEditDatabaseByName(racompListOfSSHDB[0].name); + await databaseHelper.clickOnEditDatabaseByName(racompListOfSSHDB[0].name); // Verify that user can import the SSH parameters with Password await t.expect(myRedisDatabasePage.AddRedisDatabase.sshHostInput.value).eql(racompListOfSSHDB[0].sshHost, 'SSH host import incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.sshPortInput.value).eql((racompListOfSSHDB[0].sshPort).toString(), 'SSH port import incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.sshUsernameInput.value).eql(racompListOfSSHDB[0].sshUser, 'SSH username import incorrect'); await t.expect(myRedisDatabasePage.AddRedisDatabase.sshPasswordInput.value).eql(racompListOfSSHDB[0].sshPassword, 'SSH password import incorrect'); - await clickOnEditDatabaseByName(racompListOfSSHDB[1].name); + await databaseHelper.clickOnEditDatabaseByName(racompListOfSSHDB[1].name); // Verify that user can import the SSH Private Key both by its value specified in the file and by the file path await t.expect(myRedisDatabasePage.AddRedisDatabase.sshPrivateKeyInput.textContent).contains(sshPrivateKey, 'SSH Private key import incorrect'); // Verify that user can import the SSH parameters with Passcode 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 6bb364fd10..cb7eed7b03 100644 --- a/tests/e2e/tests/critical-path/database/logical-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/logical-databases.e2e.ts @@ -1,19 +1,20 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTerms, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); fixture `Logical databases` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }) .afterEach(async() => { //Delete database - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseHelper.deleteDatabase(ossStandaloneConfig.databaseName); }); test('Verify that user can add DB with logical index via host and port from Add DB manually form', async t => { const index = '10'; diff --git a/tests/e2e/tests/critical-path/database/modules.e2e.ts b/tests/e2e/tests/critical-path/database/modules.e2e.ts index d5b2fb865a..3ad853f128 100644 --- a/tests/e2e/tests/critical-path/database/modules.e2e.ts +++ b/tests/e2e/tests/critical-path/database/modules.e2e.ts @@ -1,12 +1,14 @@ import { Selector } from 'testcafe'; import { rte, env } from '../../../helpers/constants'; -import { acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; -import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const moduleNameList = ['RediSearch', 'RedisJSON', 'RedisGraph', 'RedisTimeSeries', 'RedisBloom', 'RedisGears', 'RedisAI']; const moduleList = [myRedisDatabasePage.moduleSearchIcon, myRedisDatabasePage.moduleJSONIcon, myRedisDatabasePage.moduleGraphIcon, myRedisDatabasePage.moduleTimeseriesIcon, myRedisDatabasePage.moduleBloomIcon, myRedisDatabasePage.moduleGearsIcon, myRedisDatabasePage.moduleAIIcon]; @@ -15,16 +17,16 @@ fixture `Database modules` .meta({ type: 'critical_path' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); - await addNewStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneRedisearch); // Reload Page await browserPage.reloadPage(); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); -test.skip +test .meta({ rte: rte.standalone, env: env.web })('Verify that user can see DB modules on DB list page for Standalone DB', async t => { // Check module column on DB list page await t.expect(myRedisDatabasePage.moduleColumn.exists).ok('Module column not found'); @@ -48,7 +50,7 @@ test.skip //Verify that user can hover over the module icons and see tooltip with version. await myRedisDatabasePage.checkModulesInTooltip(moduleNameList); }); -test.skip +test .meta({ rte: rte.standalone })('Verify that user can see full module list in the Edit mode', async t => { // Verify that module column is displayed await t.expect(myRedisDatabasePage.moduleColumn.visible).ok('Module column not found'); @@ -59,7 +61,7 @@ test.skip // Verify modules in Edit mode await myRedisDatabasePage.checkModulesOnPage(moduleList); }); -test.skip +test .meta({ rte: rte.standalone })('Verify that user can see icons in DB header for RediSearch, RedisGraph, RedisJSON, RedisBloom, RedisTimeSeries, RedisGears, RedisAI default modules', async t => { // Connect to DB await myRedisDatabasePage.clickOnDBByName(ossStandaloneRedisearch.databaseName); diff --git a/tests/e2e/tests/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts b/tests/e2e/tests/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts index 68c0093939..07d2614220 100644 --- a/tests/e2e/tests/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts +++ b/tests/e2e/tests/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts @@ -1,24 +1,26 @@ -import { join } from 'path'; -import * as os from 'os'; +// import { join } from 'path'; +// import * as os from 'os'; import * as fs from 'fs'; import * as editJsonFile from 'edit-json-file'; -import { acceptLicenseTermsAndAddDatabaseApi} from '../../../helpers/database'; +import { DatabaseHelper} from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig, workingDirectory } from '../../../helpers/conf'; import { rte, env } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); if (fs.existsSync(workingDirectory)) { // Guides content const guidesTimestampPath = `${workingDirectory}/guides/build.json`; - const guidesGraphIntroductionFilePath = `${workingDirectory}/guides/quick-guides/graph/introduction.md`; + // const guidesGraphIntroductionFilePath = `${workingDirectory}/guides/quick-guides/graph/introduction.md`; // Tutorials content const tutorialsTimestampPath = `${workingDirectory}/tutorials/build.json`; - const tutorialsTimeSeriesFilePath = `${workingDirectory}/tutorials/redis_stack/redis_for_time_series.md`; + // const tutorialsTimeSeriesFilePath = `${workingDirectory}/tutorials/redis_stack/redis_for_time_series.md`; // Remove md files from local folder. When desktop tests are started, files will be updated from remote repository // Need to uncomment when desktop tests are started @@ -41,10 +43,10 @@ if (fs.existsSync(workingDirectory)) { .meta({ type: 'critical_path' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .meta({ rte: rte.standalone, env: env.desktop })('Verify that user can see updated info in Enablement Area', async t => { diff --git a/tests/e2e/tests/critical-path/files-auto-update/promo-button-autoupdate.e2e.ts b/tests/e2e/tests/critical-path/files-auto-update/promo-button-autoupdate.e2e.ts index ea2957ae66..7b9dc10463 100644 --- a/tests/e2e/tests/critical-path/files-auto-update/promo-button-autoupdate.e2e.ts +++ b/tests/e2e/tests/critical-path/files-auto-update/promo-button-autoupdate.e2e.ts @@ -1,14 +1,15 @@ -import { join } from 'path'; -import * as os from 'os'; +// import { join } from 'path'; +// import * as os from 'os'; import * as fs from 'fs'; import { Chance } from 'chance'; import * as editJsonFile from 'edit-json-file'; -import { acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, workingDirectory } from '../../../helpers/conf'; import { env } from '../../../helpers/constants'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); const chance = new Chance(); if (fs.existsSync(workingDirectory)) { @@ -31,7 +32,7 @@ if (fs.existsSync(workingDirectory)) { .meta({type: 'critical_path'}) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); test .meta({ env: env.desktop })('Verify that user has the ability to update "Create free database" button without changing the app', async t => { 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 d24b60da46..6983ba8cc8 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 @@ -1,15 +1,17 @@ import { Chance } from 'chance'; import { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage, WorkbenchPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { verifySearchFilterValue } from '../../../helpers/keys'; const memoryEfficiencyPage = new MemoryEfficiencyPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const chance = new Chance(); const hashKeyName = 'test:Hash1'; @@ -25,12 +27,12 @@ fixture `Memory Efficiency` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Analysis Tools page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); }) .afterEach(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('No reports/keys message and report tooltip', async t => { const noReportsMessage = 'No Reports foundRun "New Analysis" to generate first report.'; @@ -53,7 +55,7 @@ test('No reports/keys message and report tooltip', async t => { }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.addHashKey(hashKeyName, keysTTL[2], hashValue); await browserPage.addStreamKey(streamKeyName, 'field', 'value', keysTTL[2]); await browserPage.addStreamKey(streamKeyNameDelimiter, 'field', 'value', keysTTL[2]); @@ -72,7 +74,7 @@ test await browserPage.deleteKeyByName(hashKeyName); await browserPage.deleteKeyByName(streamKeyName); await browserPage.deleteKeyByName(streamKeyNameDelimiter); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Keyspaces displaying in Summary per keyspaces table', async t => { const noNamespacesMessage = 'No namespaces to displayConfigure the delimiter in Tree View to customize the namespaces displayed.'; @@ -129,7 +131,7 @@ test }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.addHashKey(keySpaces[4], keysTTL[2], hashValue); await browserPage.Cli.addKeysFromCliWithDelimiter('MSET', 5); await t.click(browserPage.treeViewButton); @@ -141,7 +143,7 @@ test await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await t.click(browserPage.browserViewButton); await browserPage.deleteKeyByName(keySpaces[4]); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Namespaces sorting', async t => { // Create new report await t.click(memoryEfficiencyPage.newReportBtn); @@ -174,7 +176,7 @@ test }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.addHashKey(hashKeyName, keysTTL[2], hashValue); await t.click(browserPage.treeViewButton); // Go to Analysis Tools page @@ -184,7 +186,7 @@ test await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await t.click(browserPage.browserViewButton); await browserPage.deleteKeyByName(hashKeyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Memory efficiency context saved', async t => { // Create new report await t.click(memoryEfficiencyPage.newReportBtn); @@ -201,7 +203,7 @@ test }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.addHashKey(hashKeyName, keysTTL[0], hashValue); await browserPage.addStreamKey(streamKeyName, 'field', 'value', keysTTL[1]); await browserPage.addStreamKey(streamKeyNameDelimiter, 'field', 'value'); @@ -213,7 +215,7 @@ test await browserPage.deleteKeyByName(hashKeyName); await browserPage.deleteKeyByName(streamKeyName); await browserPage.deleteKeyByName(streamKeyNameDelimiter); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Summary per expiration time', async t => { const yAxis = 218; // Create new report @@ -235,12 +237,12 @@ test }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); }) .after(async() => { await browserPage.Cli.sendCommandInCli(`del ${keyNamesReport.join(' ')}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Analysis history', async t => { const numberOfKeys: string[] = []; const dbSize = (await browserPage.Cli.getSuccessCommandResultFromCli('dbsize')).split(' '); diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index b6f2f9e56a..8130d56c98 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -1,8 +1,8 @@ import { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage, WorkbenchPage } from '../../../pageObjects'; import { RecommendationIds, rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi, deleteCustomDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig, ossStandaloneV5Config } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { RecommendationsActions } from '../../../common-actions/recommendations-actions'; import { Common } from '../../../helpers/common'; @@ -11,8 +11,10 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const recommendationsActions = new RecommendationsActions(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); -const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/'; +// const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/'; let keyName = `recomKey-${Common.generateWord(10)}`; const stringKeyName = `smallStringKey-${Common.generateWord(5)}`; const index = '1'; @@ -21,12 +23,12 @@ const useSmallerKeysRecommendation = RecommendationIds.useSmallerKeys; const avoidLogicalDbRecommendation = RecommendationIds.avoidLogicalDatabases; const redisVersionRecommendation = RecommendationIds.redisVersion; const searchJsonRecommendation = RecommendationIds.searchJson; -fixture `Memory Efficiency Recommendations` +fixture `Memory Efficiency Recommendations` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Analysis Tools page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); }) @@ -34,11 +36,11 @@ fixture `Memory Efficiency Recommendations` // Clear and delete database await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); // Go to Analysis Tools page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); // Add cached scripts and generate new report @@ -49,7 +51,7 @@ test }) .after(async() => { await browserPage.Cli.sendCommandInCli('SCRIPT FLUSH'); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Recommendations displaying', async t => { await t.click(memoryEfficiencyPage.newReportBtn); // Verify that user can see Avoid dynamic Lua script recommendation when number_of_cached_scripts> 10 @@ -95,7 +97,7 @@ test.skip('No recommendations message', async t => { }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); keyName = `recomKey-${Common.generateWord(10)}`; await browserPage.addStringKey(stringKeyName, '2147476121', 'field'); await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); @@ -107,10 +109,10 @@ test // Clear and delete database await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await browserPage.deleteKeyByName(keyName); - await deleteCustomDatabase(`${ossStandaloneConfig.databaseName} [${index}]`); + await databaseHelper.deleteCustomDatabase(`${ossStandaloneConfig.databaseName} [${index}]`); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await browserPage.deleteKeyByName(stringKeyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Avoid using logical databases recommendation', async t => { // Go to Analysis Tools page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); @@ -125,13 +127,13 @@ test }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config, ossStandaloneV5Config.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config); // Go to Analysis Tools page and create new report and open recommendations await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); await t.click(memoryEfficiencyPage.newReportBtn); await t.click(memoryEfficiencyPage.recommendationsTab); }).after(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); })('Verify that user can upvote recommendations', async t => { const notUsefulVoteOption = 'not useful'; const usefulVoteOption = 'useful'; @@ -153,7 +155,7 @@ test }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); keyName = `recomKey-${Common.generateWord(10)}`; const jsonValue = '{"name":"xyz"}'; await browserPage.addJsonKey(keyName, jsonValue); @@ -170,7 +172,7 @@ test // Clear and delete database await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can see the Tutorial opened when clicking on "Tutorial" for recommendations', async t => { // Verify that Optimize the use of time series recommendation displayed await t.expect(await memoryEfficiencyPage.getRecommendationByName(searchJsonRecommendation).exists).ok('Query and search JSON documents recommendation not displayed'); diff --git a/tests/e2e/tests/critical-path/memory-efficiency/top-keys-table.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/top-keys-table.e2e.ts index 069dc04321..f0eef2246a 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/top-keys-table.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/top-keys-table.e2e.ts @@ -2,15 +2,17 @@ import { Chance } from 'chance'; import { Selector } from 'testcafe'; import { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { deleteAllKeysFromDB, populateDBWithHashes, populateHashWithFields } from '../../../helpers/keys'; import { Common } from '../../../helpers/common'; const memoryEfficiencyPage = new MemoryEfficiencyPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const chance = new Chance(); const keyToAddParameters = { keysCount: 13, keyNameStartWith: 'hashKey' }; @@ -29,7 +31,7 @@ fixture `Memory Efficiency Top Keys Table` .page(commonUrl); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Create keys await populateDBWithHashes(ossStandaloneRedisearch.host, ossStandaloneRedisearch.port, keyToAddParameters); // Go to Analysis Tools page @@ -38,7 +40,7 @@ test .after(async() => { await browserPage.Cli.sendCommandInCli('flushdb'); await deleteAllKeysFromDB(ossStandaloneRedisearch.host, ossStandaloneRedisearch.port); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Top Keys displaying in Summary of big keys', async t => { // Verify that user can see “-” as length for all unsupported data types await browserPage.Cli.sendCommandInCli(mbloomCommand); @@ -70,7 +72,7 @@ test }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Create keys await populateHashWithFields(ossStandaloneRedisearch.host, ossStandaloneRedisearch.port, keyToAddParameters2); // Go to Analysis Tools page @@ -79,7 +81,7 @@ test .after(async t => { await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Big highlighted key tooltip', async t => { const tooltipText = 'Consider splitting it into multiple keys'; diff --git a/tests/e2e/tests/critical-path/monitor/monitor.e2e.ts b/tests/e2e/tests/critical-path/monitor/monitor.e2e.ts index 54b141db3f..0ff8f20d20 100644 --- a/tests/e2e/tests/critical-path/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/critical-path/monitor/monitor.e2e.ts @@ -1,20 +1,19 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage, BrowserPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const keyName = `${Common.generateWord(20)}-key`; const keyValue = `${Common.generateWord(10)}-value`; @@ -23,12 +22,12 @@ fixture `Monitor` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }); test .after(async() => { await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can work with Monitor', async t => { const command = 'set'; //Verify that user can open Monitor @@ -50,7 +49,7 @@ test .after(async t => { await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can see the list of all commands from all clients ran for this Redis database in the list of results in Monitor', async t => { //Define commands in different clients const cli_command = 'command'; diff --git a/tests/e2e/tests/critical-path/monitor/save-commands.e2e.ts b/tests/e2e/tests/critical-path/monitor/save-commands.e2e.ts index 20a1e3a859..9ab2fd3257 100644 --- a/tests/e2e/tests/critical-path/monitor/save-commands.e2e.ts +++ b/tests/e2e/tests/critical-path/monitor/save-commands.e2e.ts @@ -1,16 +1,19 @@ import * as fs from 'fs'; import * as os from 'os'; import { join as joinPath } from 'path'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const tempDir = os.tmpdir(); let downloadedFilePath = ''; @@ -36,12 +39,12 @@ fixture `Save commands` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); downloadedFilePath = await getFileDownloadPath(); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see a tooltip and toggle that allows to save Profiler log or not in the Profiler', async t => { // const toolTip = [ diff --git a/tests/e2e/tests/critical-path/notifications/notification-center.e2e.ts b/tests/e2e/tests/critical-path/notifications/notification-center.e2e.ts index 3b3bdce12c..b8c5aeb4a1 100644 --- a/tests/e2e/tests/critical-path/notifications/notification-center.e2e.ts +++ b/tests/e2e/tests/critical-path/notifications/notification-center.e2e.ts @@ -1,4 +1,4 @@ -import { acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { deleteAllNotificationsFromDB } from '../../../helpers/notifications'; import { commonUrl } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; @@ -10,6 +10,7 @@ const jsonNotifications: NotificationParameters[] = description.notifications; const myRedisDatabasePage = new MyRedisDatabasePage(); const settingsPage = new SettingsPage(); +const databaseHelper = new DatabaseHelper(); const NotificationPanel = myRedisDatabasePage.NavigationPanel.NotificationPanel; // Sort all notifications in json file @@ -19,7 +20,7 @@ fixture `Notifications` .meta({ rte: rte.none, type: 'critical_path' }) .page(commonUrl) .beforeEach(async(t) => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); await t.click(myRedisDatabasePage.NavigationPanel.settingsButton); await settingsPage.changeNotificationsSwitcher(true); await deleteAllNotificationsFromDB(); @@ -114,7 +115,7 @@ test('Verify that all messages in notification center are sorted by timestamp fr }); test .before(async t => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); await settingsPage.changeNotificationsSwitcher(false); await deleteAllNotificationsFromDB(); await myRedisDatabasePage.reloadPage(); diff --git a/tests/e2e/tests/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts b/tests/e2e/tests/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts index b6499788f4..47ce74475e 100644 --- a/tests/e2e/tests/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts +++ b/tests/e2e/tests/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts @@ -1,24 +1,26 @@ -import { acceptLicenseTermsAndAddDatabaseApi, acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, PubSubPage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; import { verifyMessageDisplayingInPubSub } from '../../../helpers/pub-sub'; -import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const pubSubPage = new PubSubPage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Subscribe/Unsubscribe from a channel` .meta({ env: env.web, rte: rte.standalone, type: 'critical_path' }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); //Go to PubSub page await t.click(myRedisDatabasePage.NavigationPanel.pubSubButton); }) .afterEach(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that when user subscribe to the pubsub channel he can see all the messages being published to my database from the moment of my subscription', async t => { // Verify that the Channel field placeholder is 'Enter Channel Name' @@ -64,17 +66,17 @@ test('Verify that the focus gets always shifted to a newest message (auto-scroll }); test .before(async t => { - await acceptLicenseTerms(); - await addNewStandaloneDatabaseApi(ossStandaloneV5Config); - await addNewStandaloneDatabaseApi(ossStandaloneConfig); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig); await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); // Go to PubSub page await t.click(myRedisDatabasePage.NavigationPanel.pubSubButton); }) .after(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user subscription state is changed to unsubscribed, all the messages are cleared and total message counter is reset when user connect to another database', async t => { await t.click(pubSubPage.subscribeButton); // Publish 10 messages diff --git a/tests/e2e/tests/critical-path/settings/settings.e2e.ts b/tests/e2e/tests/critical-path/settings/settings.e2e.ts index d10a9fddd5..17b51cd6bb 100644 --- a/tests/e2e/tests/critical-path/settings/settings.e2e.ts +++ b/tests/e2e/tests/critical-path/settings/settings.e2e.ts @@ -1,10 +1,11 @@ import { MyRedisDatabasePage, SettingsPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl } from '../../../helpers/conf'; const myRedisDatabasePage = new MyRedisDatabasePage(); const settingsPage = new SettingsPage(); +const databaseHelper = new DatabaseHelper(); const explicitErrorHandler = (): void => { window.addEventListener('error', e => { @@ -19,7 +20,7 @@ fixture `Settings` .page(commonUrl) .clientScripts({ content: `(${explicitErrorHandler.toString()})()` }) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); test('Verify that user can customize a number of keys to scan in filters per key name or key type', async t => { // Go to Settings page diff --git a/tests/e2e/tests/critical-path/slow-log/slow-log.e2e.ts b/tests/e2e/tests/critical-path/slow-log/slow-log.e2e.ts index 32240d5bfb..83c4e8b675 100644 --- a/tests/e2e/tests/critical-path/slow-log/slow-log.e2e.ts +++ b/tests/e2e/tests/critical-path/slow-log/slow-log.e2e.ts @@ -1,13 +1,16 @@ import { SlowLogPage, MyRedisDatabasePage, BrowserPage, ClusterDetailsPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossStandaloneBigConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const slowLogPage = new SlowLogPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const overviewPage = new ClusterDetailsPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const slowerThanParameter = 1; let maxCommandLength = 50; let command = `slowlog get ${maxCommandLength}`; @@ -16,13 +19,13 @@ fixture `Slow Log` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); await t.click(slowLogPage.slowLogTab); }) .afterEach(async() => { await slowLogPage.resetToDefaultConfig(); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test('Verify that user can open new Slow Log page using new icon on left app panel', async t => { // Verify that user see "Slow Log" page by default for non OSS Cluster diff --git a/tests/e2e/tests/critical-path/tree-view/delimiter.e2e.ts b/tests/e2e/tests/critical-path/tree-view/delimiter.e2e.ts index 3eef290ae9..e74e744942 100644 --- a/tests/e2e/tests/critical-path/tree-view/delimiter.e2e.ts +++ b/tests/e2e/tests/critical-path/tree-view/delimiter.e2e.ts @@ -1,24 +1,21 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { - BrowserPage -} from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneBigConfig -} from '../../../helpers/conf'; +import { DatabaseHelper } from '../../../helpers/database'; +import { BrowserPage } from '../../../pageObjects'; +import { commonUrl, ossStandaloneBigConfig } from '../../../helpers/conf'; import {rte} from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Delimiter tests` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .afterEach(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test('Verify that user can see that input is not saved when the Cancel button is clicked', async t => { // Switch to tree view diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts index b52d43f326..33056ada5f 100644 --- a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -1,35 +1,33 @@ import { Selector, t } from 'testcafe'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { - BrowserPage -} from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { DatabaseHelper } from '../../../helpers/database'; +import { BrowserPage } from '../../../pageObjects'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { KeyTypesTexts, rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList } from '../../../helpers/keys'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + let keyNames: string[]; let keyName1: string; let keyName2: string; let keyNameSingle: string; let index: string; -fixture`Tree view navigations improvement tests` +fixture `Tree view navigations improvement tests` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { await t.click(browserPage.patternModeBtn); await browserPage.deleteKeysByNames(keyNames); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Tree view preselected folder', async t => { keyName1 = Common.generateWord(10); // used to create index name keyName2 = Common.generateWord(10); // used to create index name @@ -108,17 +106,17 @@ test // Filtered Tree view preselected folder await t.expect(browserPage.keyListTable.textContent).notContains('No results found.', 'Key is not found message still displayed'); await t.expect( - firstTreeItemKeys.visible) + firstTreeItemKeys.exists) .notOk('First folder is expanded'); }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${index}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify tree view navigation for index based search', async t => { keyName1 = Common.generateWord(10); // used to create index name keyName2 = Common.generateWord(10); // used to create index name @@ -150,15 +148,15 @@ test test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { await t.click(browserPage.patternModeBtn); await browserPage.deleteKeysByNames(keyNames.slice(1)); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Search capability Refreshed Tree view preselected folder', async t => { - keyName1 = Common.generateWord(10); // used to create index name - keyName2 = Common.generateWord(10); // used to create index name + keyName1 = Common.generateWord(10); + keyName2 = Common.generateWord(10); keyNameSingle = Common.generateWord(10); keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle]; const commands = [ diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view.e2e.ts b/tests/e2e/tests/critical-path/tree-view/tree-view.e2e.ts index 533fcc15c7..14d12ddd61 100644 --- a/tests/e2e/tests/critical-path/tree-view/tree-view.e2e.ts +++ b/tests/e2e/tests/critical-path/tree-view/tree-view.e2e.ts @@ -1,26 +1,26 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneBigConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneBigConfig } from '../../../helpers/conf'; import { rte, KeyTypesTexts } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; import { verifySearchFilterValue } from '../../../helpers/keys'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const keyNameFilter = `keyName${Common.generateWord(10)}`; fixture `Tree view verifications` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test('Verify that user can see that "Tree view" mode is enabled state is saved when refreshes the page', async t => { // Verify that when user opens the application he can see that Tree View is disabled by default(Browser is selected by default) @@ -39,7 +39,7 @@ test .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyNameFilter); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that when user enables filtering by key name he can see only folder with appropriate keys are displayed and the number of keys and percentage is recalculated', async t => { await browserPage.addHashKey(keyNameFilter); await t.click(browserPage.treeViewButton); diff --git a/tests/e2e/tests/critical-path/workbench/autocomplete.e2e.ts b/tests/e2e/tests/critical-path/workbench/autocomplete.e2e.ts index 432efc4e31..a54cf37275 100644 --- a/tests/e2e/tests/critical-path/workbench/autocomplete.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/autocomplete.e2e.ts @@ -1,23 +1,25 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Autocomplete for entered commands` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that when user have selected a command (via “Enter” from the list of auto-suggested commands), user can see the required arguments inserted to the Editor', async t => { const commandArguments = [ diff --git a/tests/e2e/tests/critical-path/workbench/command-results.e2e.ts b/tests/e2e/tests/critical-path/workbench/command-results.e2e.ts index 185cec6d17..d05ebc2fc0 100644 --- a/tests/e2e/tests/critical-path/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/command-results.e2e.ts @@ -1,12 +1,14 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const commandForSend1 = 'info'; const commandForSend2 = 'FT._LIST'; @@ -16,13 +18,13 @@ fixture `Command results at Workbench` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async t => { await t.switchToMainWindow(); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see re-run icon near the already executed command and re-execute the command by clicking on the icon in Workbench page', async t => { // Send commands @@ -157,7 +159,7 @@ test test .after(async() => { //Drop database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can populate commands in Editor from history by clicking keyboard “up” button', async t => { const commands = [ 'FT.INFO', diff --git a/tests/e2e/tests/critical-path/workbench/context.e2e.ts b/tests/e2e/tests/critical-path/workbench/context.e2e.ts index d16cbb800a..9a63341570 100644 --- a/tests/e2e/tests/critical-path/workbench/context.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/context.e2e.ts @@ -1,12 +1,14 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const speed = 0.4; let indexName = Common.generateWord(5); @@ -15,14 +17,14 @@ fixture `Workbench Context` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Drop index, documents and database await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see saved input in Editor when navigates away to any other page', async t => { indexName = Common.generateWord(5); diff --git a/tests/e2e/tests/critical-path/workbench/cypher.e2e.ts b/tests/e2e/tests/critical-path/workbench/cypher.e2e.ts index 183c596fdc..a867146201 100644 --- a/tests/e2e/tests/critical-path/workbench/cypher.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/cypher.e2e.ts @@ -1,23 +1,25 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Cypher syntax at Workbench` .meta({type: 'critical_path', rte: rte.standalone}) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Drop database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see popover Editor when clicks on “Use Cypher Syntax” popover in the Editor or “Shift+Space”', async t => { const command = 'GRAPH.QUERY graph'; diff --git a/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts index 46bbf5c1bf..6a329d0b58 100644 --- a/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts @@ -1,13 +1,15 @@ import { Chance } from 'chance'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Telemetry } from '../../../helpers/telemetry'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const chance = new Chance(); const telemetry = new Telemetry(); @@ -25,7 +27,7 @@ fixture `Default scripts area at Workbench` .meta({type: 'critical_path', rte: rte.standalone}) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) @@ -33,7 +35,7 @@ fixture `Default scripts area at Workbench` // Drop index, documents and database await t.switchToMainWindow(); await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); test .requestHooks(logger)('Verify that user can edit and run automatically added "FT._LIST" and "FT.INFO {index}" scripts in Workbench and see the results', async t => { diff --git a/tests/e2e/tests/critical-path/workbench/index-schema.e2e.ts b/tests/e2e/tests/critical-path/workbench/index-schema.e2e.ts index 8e2e29eee4..4670726609 100644 --- a/tests/e2e/tests/critical-path/workbench/index-schema.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/index-schema.e2e.ts @@ -1,12 +1,14 @@ import { rte, env } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let indexName = Common.generateWord(5); @@ -14,7 +16,7 @@ fixture `Index Schema at Workbench` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) @@ -22,7 +24,7 @@ fixture `Index Schema at Workbench` // Drop index, documents and database await t.switchToMainWindow(); await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); test .meta({ env: env.desktop })('Verify that user can open results in Text and Table views for FT.INFO for Hash in Workbench', async t => { diff --git a/tests/e2e/tests/critical-path/workbench/json-workbench.e2e.ts b/tests/e2e/tests/critical-path/workbench/json-workbench.e2e.ts index fc9b6fecab..f533d55e5d 100644 --- a/tests/e2e/tests/critical-path/workbench/json-workbench.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/json-workbench.e2e.ts @@ -1,12 +1,14 @@ import { env, rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let indexName = Common.generateWord(5); @@ -14,7 +16,7 @@ fixture `JSON verifications at Workbench` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) @@ -22,7 +24,7 @@ fixture `JSON verifications at Workbench` // Drop index, documents and database await t.switchToMainWindow(); await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); test .meta({ env: env.desktop })('Verify that user can see result in Table and Text view for JSON data types for FT.AGGREGATE command in Workbench', async t => { diff --git a/tests/e2e/tests/critical-path/workbench/redisearch-module-not-available.e2e.ts b/tests/e2e/tests/critical-path/workbench/redisearch-module-not-available.e2e.ts index 640f30e77a..d0b26dfc90 100644 --- a/tests/e2e/tests/critical-path/workbench/redisearch-module-not-available.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/redisearch-module-not-available.e2e.ts @@ -1,11 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneV5Config } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const commandForSend = 'FT._LIST'; @@ -13,13 +15,13 @@ fixture `Redisearch module not available` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config, ossStandaloneV5Config.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); }); test('Verify that user can see the information message that the RediSearch module is not available when he runs any input with "FT." prefix in Workbench', async t => { // Send command with 'FT.' diff --git a/tests/e2e/tests/critical-path/workbench/scripting-area.e2e.ts b/tests/e2e/tests/critical-path/workbench/scripting-area.e2e.ts index 92488e9310..595bf954a6 100644 --- a/tests/e2e/tests/critical-path/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/scripting-area.e2e.ts @@ -1,12 +1,14 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let indexName = Common.generateWord(5); let keyName = Common.generateWord(5); @@ -15,7 +17,7 @@ fixture `Scripting area at Workbench` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); //Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) @@ -23,7 +25,7 @@ fixture `Scripting area at Workbench` await t.switchToMainWindow(); //Drop index, documents and database await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); // Update after resolving https://redislabs.atlassian.net/browse/RI-3299 test('Verify that user can resize scripting area in Workbench', async t => { diff --git a/tests/e2e/tests/regression/browser/add-keys.e2e.ts b/tests/e2e/tests/regression/browser/add-keys.e2e.ts index 44cbf24166..2badd61303 100644 --- a/tests/e2e/tests/regression/browser/add-keys.e2e.ts +++ b/tests/e2e/tests/regression/browser/add-keys.e2e.ts @@ -1,13 +1,16 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; import { BrowserActions } from '../../../common-actions/browser-actions'; const browserPage = new BrowserPage(); const browserActions = new BrowserActions(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const jsonKeys = [['JSON-string', '"test"'], ['JSON-number', '782364'], ['JSON-boolean', 'true'], ['JSON-null', 'null'], ['JSON-array', '[1, 2, 3]']]; let keyNames: string[]; let indexName: string; @@ -19,7 +22,7 @@ fixture `Add keys` }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { let commandString = 'DEL'; @@ -27,7 +30,7 @@ fixture `Add keys` commandString = commandString.concat(` ${key[0]}`); } await browserPage.Cli.sendCommandInCli(commandString); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can create different types(string, number, null, array, boolean) of JSON', async t => { for (let i = 0; i < jsonKeys.length; i++) { @@ -51,7 +54,7 @@ test('Verify that user can create different types(string, number, null, array, b // https://redislabs.atlassian.net/browse/RI-3995 test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .after(async() => { let commandString = 'DEL'; @@ -60,7 +63,7 @@ test } const commands = [`FT.DROPINDEX ${indexName}`, commandString]; await browserPage.Cli.sendCommandsInCli(commands); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that the new key is displayed at the top of the list', async t => { const keyName = Common.generateWord(12); const keyName1 = Common.generateWord(12); diff --git a/tests/e2e/tests/regression/browser/consumer-group.e2e.ts b/tests/e2e/tests/regression/browser/consumer-group.e2e.ts index b3a0b6edff..3c7ef2dc76 100644 --- a/tests/e2e/tests/regression/browser/consumer-group.e2e.ts +++ b/tests/e2e/tests/regression/browser/consumer-group.e2e.ts @@ -1,14 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(20); let consumerGroupName = Common.generateWord(20); @@ -19,7 +18,7 @@ fixture `Consumer group` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async t => { // Clear and delete database @@ -27,7 +26,7 @@ fixture `Consumer group` await t.click(browserPage.closeKeyButton); } await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that when user enter invalid Group Name the error message appears', async t => { const message = 'Your Key has no Consumer Groups available.'; diff --git a/tests/e2e/tests/regression/browser/context.e2e.ts b/tests/e2e/tests/regression/browser/context.e2e.ts index 1861afe0cf..81a5068ca7 100644 --- a/tests/e2e/tests/regression/browser/context.e2e.ts +++ b/tests/e2e/tests/regression/browser/context.e2e.ts @@ -1,16 +1,18 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; import { verifySearchFilterValue } from '../../../helpers/keys'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); @@ -18,12 +20,12 @@ fixture `Browser Context` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that if user has saved context on Browser page and go to Settings page, Browser and Workbench icons are displayed and user is able to open Browser with saved context', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/regression/browser/filtering-iteratively.e2e.ts b/tests/e2e/tests/regression/browser/filtering-iteratively.e2e.ts index f835dc1063..59ea2b06b3 100644 --- a/tests/e2e/tests/regression/browser/filtering-iteratively.e2e.ts +++ b/tests/e2e/tests/regression/browser/filtering-iteratively.e2e.ts @@ -1,11 +1,13 @@ -import { acceptLicenseTermsAndAddDatabaseApi, acceptLicenseTermsAndAddOSSClusterDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossClusterConfig, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; import { KeyTypesTexts, rte } from '../../../helpers/constants'; -import { deleteOSSClusterDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keys: string[]; @@ -13,12 +15,12 @@ fixture `Filtering iteratively in Browser page` .meta({ type: 'regression' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .meta({ rte: rte.standalone })('Verify that user can see search results per 500 keys if number of results is 500', async t => { @@ -49,12 +51,12 @@ test test .meta({ rte: rte.ossCluster }) .before(async() => { - await acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig, ossClusterConfig.ossClusterDatabaseName); + await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig); }) .after(async() => { // Clear and delete database await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`); - await deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); })('Verify that user can search via Scan more for search pattern and selected data type in OSS Cluster DB', async t => { // Create new keys keys = await Common.createArrayWithKeyValueForOSSCluster(1000); @@ -74,11 +76,11 @@ test .meta({ rte: rte.standalone }) .before(async() => { // Add Big standalone DB - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .after(async() => { // Clear and delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user use Scan More in DB with 10-50 millions of keys (when search by pattern/)', async t => { // Search all string keys await browserPage.searchByKeyName('*'); diff --git a/tests/e2e/tests/regression/browser/filtering.e2e.ts b/tests/e2e/tests/regression/browser/filtering.e2e.ts index 20fd853630..a691309de4 100644 --- a/tests/e2e/tests/regression/browser/filtering.e2e.ts +++ b/tests/e2e/tests/regression/browser/filtering.e2e.ts @@ -1,13 +1,15 @@ import { Selector } from 'testcafe'; import { KeyTypesTexts, rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig, ossStandaloneBigConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { keyTypes } from '../../../helpers/keys'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(20); let keyName2 = Common.generateWord(20); @@ -17,12 +19,12 @@ fixture `Filtering per key name in Browser page` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that when user searches not existed key, he can see the standard screen when there are no keys found', async t => { keyName = `KeyForSearch*${Common.generateWord(10)}?[]789`; @@ -61,7 +63,7 @@ test // Clear and delete database await browserPage.deleteKeyByName(keyName); await browserPage.deleteKeyByName(keyName2); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can filter per pattern with [xy] (matches one symbol: either x or y))', async t => { keyName = `KeyForSearch${Common.generateWord(10)}`; keyName2 = `KeyForFearch${Common.generateWord(10)}`; @@ -91,7 +93,7 @@ test test .after(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that when user clicks on “clear” control with no filter per key name applied all characters and filter per key type are removed, “clear” control is disappeared', async t => { keyName = `KeyForSearch${Common.generateWord(10)}`; @@ -124,13 +126,12 @@ test }); test .before(async() => { - // Add Big standalone DB - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .after(async() => { // Delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can filter per exact key without using any patterns in DB with 10 millions of keys', async t => { // Create new key keyName = `KeyForSearch-${Common.generateWord(10)}`; @@ -147,12 +148,11 @@ test }); test .before(async() => { - // Add Big standalone DB - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .after(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can filter per key name using patterns in DB with 10-50 millions of keys', async t => { keyName = 'device*'; await browserPage.selectFilterGroupType(KeyTypesTexts.Set); diff --git a/tests/e2e/tests/regression/browser/format-switcher.e2e.ts b/tests/e2e/tests/regression/browser/format-switcher.e2e.ts index 42cb100668..3f5e4b1798 100644 --- a/tests/e2e/tests/regression/browser/format-switcher.e2e.ts +++ b/tests/e2e/tests/regression/browser/format-switcher.e2e.ts @@ -1,13 +1,15 @@ import { keyLength, rte } from '../../../helpers/constants'; import { addKeysViaCli, deleteKeysViaCli, keyTypes } from '../../../helpers/keys'; -import { acceptLicenseTerms, acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { addNewStandaloneDatabasesApi, deleteStandaloneDatabaseApi, deleteStandaloneDatabasesApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const keysData = keyTypes.map(object => ({ ...object })); keysData.forEach(key => key.keyName = `${key.keyName}` + '-' + `${Common.generateWord(keyLength)}`); @@ -23,20 +25,20 @@ fixture `Format switcher functionality` }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Create new keys await addKeysViaCli(keysData); }) .afterEach(async() => { // Clear keys and database await deleteKeysViaCli(keysData); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .before(async() => { // Add new databases using API - await acceptLicenseTerms(); - await addNewStandaloneDatabasesApi(databasesForAdding); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding); // Reload Page await browserPage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName); @@ -46,7 +48,7 @@ test .after(async() => { // Clear keys and database await deleteKeysViaCli(keysData); - await deleteStandaloneDatabasesApi(databasesForAdding); + await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding); })('Formatters saved selection', async t => { // Open key details and select JSON formatter await browserPage.openKeyDetails(keysData[0].keyName); diff --git a/tests/e2e/tests/regression/browser/formatter-warning.e2e.ts b/tests/e2e/tests/regression/browser/formatter-warning.e2e.ts index 2af964fbfe..6687c45f78 100644 --- a/tests/e2e/tests/regression/browser/formatter-warning.e2e.ts +++ b/tests/e2e/tests/regression/browser/formatter-warning.e2e.ts @@ -1,11 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const jsonInvalidStructure = '"{\"test\": 123"'; const title = 'Value will be saved as Unicode'; @@ -19,12 +21,12 @@ fixture `Warning for invalid formatter value` }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear keys and database await browserPage.Cli.sendCommandInCli(`del ${keyName}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see warning message when editing value', async t => { // Open key details diff --git a/tests/e2e/tests/regression/browser/full-screen.e2e.ts b/tests/e2e/tests/regression/browser/full-screen.e2e.ts index e8bcf2a316..fae1858b35 100644 --- a/tests/e2e/tests/regression/browser/full-screen.e2e.ts +++ b/tests/e2e/tests/regression/browser/full-screen.e2e.ts @@ -1,11 +1,13 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const keyName = Common.generateWord(20); const keyValue = Common.generateWord(20); @@ -14,21 +16,21 @@ fixture `Full Screen` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.addStringKey(keyName, keyValue); await browserPage.openKeyDetails(keyName); }) .after(async() => { await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can switch to full screen from key details in Browser', async t => { // Save tables size before switching to full screen mode const widthBeforeFullScreen = await browserPage.keyDetailsTable.clientWidth; @@ -58,13 +60,13 @@ test('Verify that when no keys are selected user can click on "Close" control fo }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.addSetKey(keyName, keyValue); await browserPage.openKeyDetails(keyName); }) .after(async() => { await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that when user closes key details in full screen mode the list of keys displayed in full screen', async t => { // Save keys table size before switching to full screen const widthKeysBeforeFullScreen = await browserPage.keyListTable.clientWidth; @@ -84,13 +86,13 @@ test }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.addHashKey(keyName, '58965422', 'filed', 'value'); await browserPage.openKeyDetails(keyName); }) .after(async() => { await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that when users close key details not in full mode, they can see full key list screen', async t => { // Save key list table size before switching to full screen const widthKeysBeforeFullScreen = await browserPage.keyListTable.clientWidth; diff --git a/tests/e2e/tests/regression/browser/handle-dbsize-permissions.e2e.ts b/tests/e2e/tests/regression/browser/handle-dbsize-permissions.e2e.ts index 86e3567d24..b942bc349e 100644 --- a/tests/e2e/tests/regression/browser/handle-dbsize-permissions.e2e.ts +++ b/tests/e2e/tests/regression/browser/handle-dbsize-permissions.e2e.ts @@ -1,17 +1,20 @@ import { t } from 'testcafe'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig, ossStandaloneNoPermissionsConfig } from '../../../helpers/conf'; -import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const createUserCommand = 'acl setuser noperm nopass on +@all ~* -dbsize'; const keyName = Common.generateWord(20); const createKeyCommand = `set ${keyName} ${Common.generateWord(20)}`; @@ -20,17 +23,17 @@ fixture `Handle user permissions` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); await browserPage.Cli.sendCommandInCli(createUserCommand); ossStandaloneNoPermissionsConfig.host = process.env.OSS_STANDALONE_BIG_HOST || 'oss-standalone-big'; await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); - await addNewStandaloneDatabaseApi(ossStandaloneNoPermissionsConfig); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneNoPermissionsConfig); await browserPage.reloadPage(); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); - await deleteStandaloneDatabaseApi(ossStandaloneNoPermissionsConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneNoPermissionsConfig); // Change config to initial ossStandaloneNoPermissionsConfig.host = process.env.OSS_STANDALONE_HOST || 'oss-standalone'; }); diff --git a/tests/e2e/tests/regression/browser/hash-field.e2e.ts b/tests/e2e/tests/regression/browser/hash-field.e2e.ts index 224755703e..60e9429d60 100644 --- a/tests/e2e/tests/regression/browser/hash-field.e2e.ts +++ b/tests/e2e/tests/regression/browser/hash-field.e2e.ts @@ -1,12 +1,14 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { populateHashWithFields } from '../../../helpers/keys'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; const keyName = `TestHashKey-${Common.generateWord(10)}`; @@ -17,13 +19,13 @@ fixture `Hash Key fields verification` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.addHashKey(keyName, '2147476121', 'field', 'value'); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can search per exact field name in Hash in DB with 1 million of fields', async t => { // Add 1000000 fields to the hash key diff --git a/tests/e2e/tests/regression/browser/key-messages.e2e.ts b/tests/e2e/tests/regression/browser/key-messages.e2e.ts index 5bc55aeb05..281901ecd4 100644 --- a/tests/e2e/tests/regression/browser/key-messages.e2e.ts +++ b/tests/e2e/tests/regression/browser/key-messages.e2e.ts @@ -1,13 +1,15 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const dataTypes: string[] = [ @@ -19,10 +21,10 @@ fixture `Key messages` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see updated message in Browser for TimeSeries and Graph data types', async t => { for(let i = 0; i < dataTypes.length; i++) { 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 642721dc1f..d6dfa49f2b 100644 --- a/tests/e2e/tests/regression/browser/keys-all-databases.e2e.ts +++ b/tests/e2e/tests/regression/browser/keys-all-databases.e2e.ts @@ -1,13 +1,6 @@ import { Selector, t } from 'testcafe'; import { env, rte } from '../../../helpers/constants'; -import { - acceptLicenseTermsAndAddDatabaseApi, - acceptLicenseTermsAndAddOSSClusterDatabase, - acceptLicenseTermsAndAddRECloudDatabase, - acceptLicenseTermsAndAddREClusterDatabase, - acceptLicenseTermsAndAddSentinelDatabaseApi, - deleteDatabase -} from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { cloudDatabaseConfig, @@ -18,16 +11,14 @@ import { redisEnterpriseClusterConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; -import { - deleteOSSClusterDatabaseApi, - deleteStandaloneDatabaseApi, - deleteAllDatabasesByConnectionTypeApi -} from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { BrowserActions } from '../../../common-actions/browser-actions'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const browserActions = new BrowserActions(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const verifyKeysAdded = async(): Promise => { @@ -50,59 +41,59 @@ fixture `Work with keys in all types of databases` test .meta({ rte: rte.reCluster }) .before(async() => { - await acceptLicenseTermsAndAddREClusterDatabase(redisEnterpriseClusterConfig); + await databaseHelper.acceptLicenseTermsAndAddREClusterDatabase(redisEnterpriseClusterConfig); }) .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(redisEnterpriseClusterConfig.databaseName); + await databaseHelper.deleteDatabase(redisEnterpriseClusterConfig.databaseName); })('Verify that user can add Key in RE Cluster DB', async() => { await verifyKeysAdded(); }); test .meta({ rte: rte.reCloud }) .before(async() => { - await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); + await databaseHelper.acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); }) .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(cloudDatabaseConfig.databaseName); + await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName); })('Verify that user can add Key in RE Cloud DB', async() => { await verifyKeysAdded(); }); test .meta({ rte: rte.ossCluster }) .before(async() => { - await acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig, ossClusterConfig.ossClusterDatabaseName); + await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig); }) .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); })('Verify that user can add Key in OSS Cluster DB', async() => { await verifyKeysAdded(); }); test .meta({ env: env.web, rte: rte.sentinel }) .before(async() => { - await acceptLicenseTermsAndAddSentinelDatabaseApi(ossSentinelConfig); + await databaseHelper.acceptLicenseTermsAndAddSentinelDatabaseApi(ossSentinelConfig); }) .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteAllDatabasesByConnectionTypeApi('SENTINEL'); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL'); })('Verify that user can add Key in Sentinel Primary Group', async() => { await verifyKeysAdded(); }); test .meta({ rte: rte.standalone }) .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .after(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can scroll key virtualized table and see keys info displayed', async() => { const listItems = browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow); const maxNumberOfScrolls = 15; diff --git a/tests/e2e/tests/regression/browser/large-key-details-values.e2e.ts b/tests/e2e/tests/regression/browser/large-key-details-values.e2e.ts index 3f2b3fb50a..0539b821cc 100644 --- a/tests/e2e/tests/regression/browser/large-key-details-values.e2e.ts +++ b/tests/e2e/tests/regression/browser/large-key-details-values.e2e.ts @@ -1,11 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const field = Common.generateWord(20); const value = Common.generateSentence(200); @@ -17,7 +19,7 @@ fixture `Expand/Collapse large values in key details` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async t => { // Clear and delete database @@ -25,7 +27,7 @@ fixture `Expand/Collapse large values in key details` await t.click(browserPage.closeKeyButton); } await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can click on a row to expand it if any of its cells contains a value which is truncated.', async t => { const entryFieldLong = browserPage.streamEntryFields.nth(1).parent(1); diff --git a/tests/e2e/tests/regression/browser/last-refresh.e2e.ts b/tests/e2e/tests/regression/browser/last-refresh.e2e.ts index 6455d8037c..d3cd279834 100644 --- a/tests/e2e/tests/regression/browser/last-refresh.e2e.ts +++ b/tests/e2e/tests/regression/browser/last-refresh.e2e.ts @@ -1,11 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); @@ -13,12 +15,12 @@ fixture `Last refresh` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see my timer updated when I refresh the list of Keys of the list of values', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/regression/browser/list-key.e2e.ts b/tests/e2e/tests/regression/browser/list-key.e2e.ts index 4b284aafb6..c39c6d2a6b 100644 --- a/tests/e2e/tests/regression/browser/list-key.e2e.ts +++ b/tests/e2e/tests/regression/browser/list-key.e2e.ts @@ -1,12 +1,14 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { populateListWithElements } from '../../../helpers/keys'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; const keyName = `TestListKey-${ Common.generateWord(10) }`; @@ -17,13 +19,13 @@ fixture `List Key verification` .meta({ type: 'regression' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.addListKey(keyName, '2147476121', 'testElement'); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .meta({ rte: rte.standalone })('Verify that user can search per exact element index in List key in DB with 1 million of fields', async t => { diff --git a/tests/e2e/tests/regression/browser/onboarding.e2e.ts b/tests/e2e/tests/regression/browser/onboarding.e2e.ts index 909edee047..0cfb562115 100644 --- a/tests/e2e/tests/regression/browser/onboarding.e2e.ts +++ b/tests/e2e/tests/regression/browser/onboarding.e2e.ts @@ -1,6 +1,4 @@ -import { - acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase -} from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossStandaloneConfigEmpty } from '../../../helpers/conf'; @@ -25,6 +23,7 @@ const workBenchPage = new WorkbenchPage(); const slowLogPage = new SlowLogPage(); const pubSubPage = new PubSubPage(); const telemetry = new Telemetry(); +const databaseHelper = new DatabaseHelper(); const logger = telemetry.createLogger(); const indexName = Common.generateWord(10); @@ -37,11 +36,11 @@ fixture `Onboarding new user tests` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfigEmpty, ossStandaloneConfigEmpty.databaseName); + await databaseHelper.acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfigEmpty); }) .afterEach(async() => { await browserPage.Cli.sendCommandInCli(`DEL ${indexName}`); - await deleteDatabase(ossStandaloneConfigEmpty.databaseName); + await databaseHelper.deleteDatabase(ossStandaloneConfigEmpty.databaseName); }); // https://redislabs.atlassian.net/browse/RI-4070, https://redislabs.atlassian.net/browse/RI-4067 // https://redislabs.atlassian.net/browse/RI-4278 diff --git a/tests/e2e/tests/regression/browser/resize-columns.e2e.ts b/tests/e2e/tests/regression/browser/resize-columns.e2e.ts index e5e9ad516d..75df30840c 100644 --- a/tests/e2e/tests/regression/browser/resize-columns.e2e.ts +++ b/tests/e2e/tests/regression/browser/resize-columns.e2e.ts @@ -1,15 +1,17 @@ -import { acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { addNewStandaloneDatabasesApi, deleteStandaloneDatabasesApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const keyName = Common.generateWord(10); const longFieldName = Common.generateSentence(20); @@ -48,8 +50,8 @@ fixture `Resize columns in Key details` .page(commonUrl) .beforeEach(async() => { // Add new databases using API - await acceptLicenseTerms(); - await addNewStandaloneDatabasesApi(databasesForAdding); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding); // Reload Page await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName); @@ -61,7 +63,7 @@ fixture `Resize columns in Key details` // Clear and delete database await browserPage.OverviewPanel.changeDbIndex(0); await browserPage.deleteKeysByNames(keyNames); - await deleteStandaloneDatabasesApi(databasesForAdding); + await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding); }); test('Resize of columns in Hash, List, Zset Key details', async t => { const field = browserPage.keyDetailsTable.find(browserPage.cssRowInVirtualizedTable); diff --git a/tests/e2e/tests/regression/browser/scan-keys.e2e.ts b/tests/e2e/tests/regression/browser/scan-keys.e2e.ts index 5b13cafcf8..044c7e0728 100644 --- a/tests/e2e/tests/regression/browser/scan-keys.e2e.ts +++ b/tests/e2e/tests/regression/browser/scan-keys.e2e.ts @@ -1,10 +1,11 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, SettingsPage } from '../../../pageObjects'; import { commonUrl } from '../../../helpers/conf'; const myRedisDatabasePage = new MyRedisDatabasePage(); const settingsPage = new SettingsPage(); +const databaseHelper = new DatabaseHelper(); const explicitErrorHandler = (): void => { window.addEventListener('error', e => { @@ -19,7 +20,7 @@ fixture `Browser - Specify Keys to Scan` .page(commonUrl) .clientScripts({ content: `(${explicitErrorHandler.toString()})()` }) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); test('Verify that the user not enter the value less than 500 - the system automatically applies min value if user enters less than min', async t => { // Go to Settings page diff --git a/tests/e2e/tests/regression/browser/set-key.e2e.ts b/tests/e2e/tests/regression/browser/set-key.e2e.ts index b2ca048e71..038eb4f44f 100644 --- a/tests/e2e/tests/regression/browser/set-key.e2e.ts +++ b/tests/e2e/tests/regression/browser/set-key.e2e.ts @@ -1,12 +1,14 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { populateSetWithMembers } from '../../../helpers/keys'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; const keyName = `TestSetKey-${ Common.generateWord(10) }`; @@ -17,13 +19,13 @@ fixture `Set Key verification` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.addSetKey(keyName, '2147476121', 'testMember'); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can search per exact member name in Set key in DB with 1 million of members', async t => { // Add 1000000 members to the set key diff --git a/tests/e2e/tests/regression/browser/stream-key.e2e.ts b/tests/e2e/tests/regression/browser/stream-key.e2e.ts index 6a837c7153..83d2974aa6 100644 --- a/tests/e2e/tests/regression/browser/stream-key.e2e.ts +++ b/tests/e2e/tests/regression/browser/stream-key.e2e.ts @@ -1,11 +1,13 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { rte } from '../../../helpers/constants'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const value = Common.generateWord(5); let field = Common.generateWord(5); @@ -18,11 +20,11 @@ fixture `Stream key` }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see a Stream in a table format', async t => { const streamFields = [ 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 0e6320a3ba..30f28454ee 100644 --- a/tests/e2e/tests/regression/browser/stream-pending-messages.e2e.ts +++ b/tests/e2e/tests/regression/browser/stream-pending-messages.e2e.ts @@ -1,14 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(20); let consumerGroupName = Common.generateWord(20); @@ -17,7 +16,7 @@ fixture `Pending messages` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async t => { // Clear and delete database @@ -25,7 +24,7 @@ fixture `Pending messages` await t.click(browserPage.closeKeyButton); } await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can\'t select currently selected Consumer to Claim message in the drop-down', async t => { keyName = Common.generateWord(20); diff --git a/tests/e2e/tests/regression/browser/survey-link.e2e.ts b/tests/e2e/tests/regression/browser/survey-link.e2e.ts index 9ba190d0b2..863473972e 100644 --- a/tests/e2e/tests/regression/browser/survey-link.e2e.ts +++ b/tests/e2e/tests/regression/browser/survey-link.e2e.ts @@ -1,12 +1,15 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { env, rte } from '../../../helpers/constants'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; // import { Common } from '../../../helpers/common'; -import { deleteAllDatabasesApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + // const externalPageLink = 'https://www.surveymonkey.com/r/redisinsight'; fixture `User Survey` @@ -17,7 +20,7 @@ fixture `User Survey` }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }); test('Verify that user can use survey link', async t => { // Verify that user can see survey link on any page inside of DB @@ -41,7 +44,7 @@ test('Verify that user can use survey link', async t => { await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); await t.expect(browserPage.userSurveyLink.exists).notOk('Survey Link is visible'); // Verify that user cannot see survey link for welcome page - await deleteAllDatabasesApi(); + await databaseAPIRequests.deleteAllDatabasesApi(); await browserPage.reloadPage(); await t.expect(browserPage.userSurveyLink.exists).notOk('Survey Link is visible'); }); diff --git a/tests/e2e/tests/regression/browser/ttl-format.e2e.ts b/tests/e2e/tests/regression/browser/ttl-format.e2e.ts index 17c0c69fcc..58884586c8 100644 --- a/tests/e2e/tests/regression/browser/ttl-format.e2e.ts +++ b/tests/e2e/tests/regression/browser/ttl-format.e2e.ts @@ -1,13 +1,15 @@ import { Selector } from 'testcafe'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { keyTypes } from '../../../helpers/keys'; import { rte, COMMANDS_TO_CREATE_KEY, keyLength } from '../../../helpers/constants'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const keyName = Common.generateWord(20); const keysData = keyTypes.map(object => ({ ...object })).slice(0, 6); @@ -25,14 +27,14 @@ fixture `TTL values in Keys Table` }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database for (let i = 0; i < keysData.length; i++) { await browserPage.deleteKey(); } - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see TTL in the list of keys rounded down to the nearest unit', async t => { // Create new keys with TTL @@ -54,7 +56,7 @@ test('Verify that user can see TTL in the list of keys rounded down to the neare }); test .after(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that Key is deleted if TTL finishes', async t => { // Create new key with TTL const TTL = 15; 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 1f6d4438e7..5cd1defe3a 100644 --- a/tests/e2e/tests/regression/browser/upload-json-key.e2e.ts +++ b/tests/e2e/tests/regression/browser/upload-json-key.e2e.ts @@ -1,12 +1,14 @@ import * as path from 'path'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const filePath = path.join('..', '..', '..', 'test-data', 'upload-json', 'sample.json'); const jsonValues = ['Live JSON generator', '3.1', '"2014-06-25T00:00:00.000Z"', 'true']; @@ -19,11 +21,11 @@ fixture `Upload json file` }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { await browserPage.Cli.sendCommandInCli(`DEL ${keyName}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); // https://redislabs.atlassian.net/browse/RI-4061 test('Verify that user can insert a JSON from .json file on the form to add a JSON key', async t => { diff --git a/tests/e2e/tests/regression/cli/cli-command-helper.e2e.ts b/tests/e2e/tests/regression/cli/cli-command-helper.e2e.ts index f8d8259e8a..58a1d8d1a4 100644 --- a/tests/e2e/tests/regression/cli/cli-command-helper.e2e.ts +++ b/tests/e2e/tests/regression/cli/cli-command-helper.e2e.ts @@ -1,14 +1,13 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { Common } from '../../../helpers/common'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { BrowserPage } from '../../../pageObjects'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let filteringGroup = ''; let filteringGroups: string[] = []; @@ -23,11 +22,11 @@ fixture `CLI Command helper` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can open/close CLI separately from Command Helper', async t => { // Open CLI diff --git a/tests/e2e/tests/regression/cli/cli-logical-db.e2e.ts b/tests/e2e/tests/regression/cli/cli-logical-db.e2e.ts index e6b797d9b9..d8f0a98d16 100644 --- a/tests/e2e/tests/regression/cli/cli-logical-db.e2e.ts +++ b/tests/e2e/tests/regression/cli/cli-logical-db.e2e.ts @@ -1,14 +1,13 @@ -import { acceptLicenseTerms, deleteCustomDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let index = '0'; let databaseEndpoint = `${ossStandaloneConfig.host}:${ossStandaloneConfig.port}`; @@ -23,16 +22,16 @@ fixture `CLI logical database` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }) .afterEach(async() => { // Delete database - await deleteCustomDatabase(`${ossStandaloneConfig.databaseName} [${index}]`); + await databaseHelper.deleteCustomDatabase(`${ossStandaloneConfig.databaseName} [${index}]`); }); test .after(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that working with logical DBs, user can not see 0 DB index in CLI', async t => { await myRedisDatabasePage.AddRedisDatabase.addLogicalRedisDatabase(ossStandaloneConfig, index); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); diff --git a/tests/e2e/tests/regression/cli/cli-promote-workbench.ts b/tests/e2e/tests/regression/cli/cli-promote-workbench.ts index d13492f720..72c50afb1b 100644 --- a/tests/e2e/tests/regression/cli/cli-promote-workbench.ts +++ b/tests/e2e/tests/regression/cli/cli-promote-workbench.ts @@ -1,25 +1,24 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Promote workbench in CLI` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see saved workbench context after redirection from CLI to workbench', async t => { // Open Workbench diff --git a/tests/e2e/tests/regression/cli/cli-re-cluster.e2e.ts b/tests/e2e/tests/regression/cli/cli-re-cluster.e2e.ts index 36b10c757e..ff2c88f80c 100644 --- a/tests/e2e/tests/regression/cli/cli-re-cluster.e2e.ts +++ b/tests/e2e/tests/regression/cli/cli-re-cluster.e2e.ts @@ -1,12 +1,6 @@ import { Selector, t } from 'testcafe'; import { env, rte } from '../../../helpers/constants'; -import { - acceptLicenseTermsAndAddOSSClusterDatabase, - acceptLicenseTermsAndAddRECloudDatabase, - acceptLicenseTermsAndAddREClusterDatabase, - acceptLicenseTermsAndAddSentinelDatabaseApi, - deleteDatabase -} from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { cloudDatabaseConfig, @@ -15,9 +9,11 @@ import { redisEnterpriseClusterConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; -import { deleteOSSClusterDatabaseApi, deleteAllDatabasesByConnectionTypeApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const verifyCommandsInCli = async(): Promise => { @@ -40,12 +36,12 @@ fixture `Work with CLI in all types of databases` test .meta({ rte: rte.reCluster }) .before(async() => { - await acceptLicenseTermsAndAddREClusterDatabase(redisEnterpriseClusterConfig); + await databaseHelper.acceptLicenseTermsAndAddREClusterDatabase(redisEnterpriseClusterConfig); }) .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(redisEnterpriseClusterConfig.databaseName); + await databaseHelper.deleteDatabase(redisEnterpriseClusterConfig.databaseName); })('Verify that user can add data via CLI in RE Cluster DB', async() => { // Verify that database index switcher not displayed for RE Cluster await t.expect(browserPage.OverviewPanel.changeIndexBtn.exists).notOk('Change Db index control displayed for RE Cluster DB'); @@ -55,12 +51,12 @@ test test .meta({ rte: rte.reCloud }) .before(async() => { - await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); + await databaseHelper.acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); }) .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(cloudDatabaseConfig.databaseName); + await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName); })('Verify that user can add data via CLI in RE Cloud DB', async() => { // Verify that database index switcher not displayed for RE Cloud await t.expect(browserPage.OverviewPanel.changeIndexBtn.exists).notOk('Change Db index control displayed for RE Cloud DB'); @@ -70,12 +66,12 @@ test test .meta({ rte: rte.ossCluster }) .before(async() => { - await acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig, ossClusterConfig.ossClusterDatabaseName); + await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig); }) .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); })('Verify that user can add data via CLI in OSS Cluster DB', async() => { // Verify that database index switcher not displayed for RE Cloud await t.expect(browserPage.OverviewPanel.changeIndexBtn.exists).notOk('Change Db index control displayed for OSS Cluster DB'); @@ -85,12 +81,12 @@ test test .meta({ env: env.web, rte: rte.sentinel }) .before(async() => { - await acceptLicenseTermsAndAddSentinelDatabaseApi(ossSentinelConfig); + await databaseHelper.acceptLicenseTermsAndAddSentinelDatabaseApi(ossSentinelConfig); }) .after(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteAllDatabasesByConnectionTypeApi('SENTINEL'); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL'); })('Verify that user can add data via CLI in Sentinel Primary Group', async() => { // Verify that database index switcher displayed for Sentinel await t.expect(browserPage.OverviewPanel.changeIndexBtn.exists).ok('Change Db index control not displayed for Sentinel DB'); diff --git a/tests/e2e/tests/regression/cli/cli.e2e.ts b/tests/e2e/tests/regression/cli/cli.e2e.ts index 7ed517e444..d43519cd19 100644 --- a/tests/e2e/tests/regression/cli/cli.e2e.ts +++ b/tests/e2e/tests/regression/cli/cli.e2e.ts @@ -1,14 +1,13 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { Common } from '../../../helpers/common'; import { BrowserPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(20); const keyTTL = '2147476121'; @@ -19,11 +18,11 @@ fixture `CLI` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see CLI is minimized when he clicks the "minimize" button', async t => { const cliColourBefore = await Common.getBackgroundColour(browserPage.Cli.cliBadge); @@ -53,7 +52,7 @@ test .after(async() => { // Clear database and delete await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can repeat commands by entering a number of repeats before the Redis command in CLI', async t => { keyName = Common.generateWord(20); const command = `SET ${keyName} a`; @@ -70,7 +69,7 @@ test .after(async() => { // Clear database and delete await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can run command json.get and see JSON object with escaped quotes (\" instead of ")', async t => { keyName = Common.generateWord(20); const jsonValueCli = '"{\\"name\\":\\"xyz\\"}"'; @@ -87,14 +86,14 @@ test }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); for (const command of cliCommands) { await browserPage.Cli.sendCommandInCli(command); } }) .after(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can use "Up" and "Down" keys to view previous commands in CLI in the application', async t => { const databaseEndpoint = `${ossStandaloneConfig.host}:${ossStandaloneConfig.port}`; diff --git a/tests/e2e/tests/regression/database-overview/database-info.e2e.ts b/tests/e2e/tests/regression/database-overview/database-info.e2e.ts index 7fdecf24f2..e9184bad0b 100644 --- a/tests/e2e/tests/regression/database-overview/database-info.e2e.ts +++ b/tests/e2e/tests/regression/database-overview/database-info.e2e.ts @@ -1,4 +1,4 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage, @@ -6,21 +6,23 @@ import { } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Database info tooltips` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see DB name, endpoint, connection type, Redis version, user name in tooltip when hover over the (i) icon', async t => { const version = /[0-9].[0-9].[0-9]/; diff --git a/tests/e2e/tests/regression/database-overview/database-overview-keys.e2e.ts b/tests/e2e/tests/regression/database-overview/database-overview-keys.e2e.ts index 002d7b186d..e7bb9ef57b 100644 --- a/tests/e2e/tests/regression/database-overview/database-overview-keys.e2e.ts +++ b/tests/e2e/tests/regression/database-overview/database-overview-keys.e2e.ts @@ -1,4 +1,4 @@ -import { acceptLicenseTermsAndAddDatabase, acceptLicenseTermsAndAddRECloudDatabase, deleteCustomDatabase, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage, @@ -7,13 +7,15 @@ import { import { rte } from '../../../helpers/constants'; import { cloudDatabaseConfig, commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { BrowserActions } from '../../../common-actions/browser-actions'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const browserPage = new BrowserPage(); const browserActions = new BrowserActions(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keys: string[]; const keyName = Common.generateWord(10); @@ -25,7 +27,7 @@ fixture `Database overview` .page(commonUrl) .beforeEach(async t => { // Create databases and keys - await acceptLicenseTermsAndAddDatabase(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabase(ossStandaloneRedisearch); await browserPage.addStringKey(keyName); await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); await myRedisDatabasePage.AddRedisDatabase.addLogicalRedisDatabase(ossStandaloneRedisearch, index); @@ -38,10 +40,10 @@ fixture `Database overview` await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); await myRedisDatabasePage.clickOnDBByName(`${ossStandaloneRedisearch.databaseName} [db${index}]`); await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`); - await deleteCustomDatabase(`${ossStandaloneRedisearch.databaseName} [db${index}]`); + await databaseHelper.deleteCustomDatabase(`${ossStandaloneRedisearch.databaseName} [db${index}]`); await myRedisDatabasePage.clickOnDBByName(ossStandaloneRedisearch.databaseName); await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); test .meta({ rte: rte.standalone })('Verify that user can see total and current logical database number of keys (if there are any keys in other logical DBs)', async t => { @@ -65,11 +67,11 @@ test test .meta({ rte: rte.reCloud }) .before(async() => { - await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); + await databaseHelper.acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); }) .after(async() => { // Delete database - await deleteDatabase(cloudDatabaseConfig.databaseName); + await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName); })('Verify that when users hover over keys icon in Overview for Cloud DB, they see only total number of keys in tooltip', async t => { await t.hover(workbenchPage.OverviewPanel.overviewTotalKeys); // Verify that user can see only total number of keys diff --git a/tests/e2e/tests/regression/database-overview/database-overview.e2e.ts b/tests/e2e/tests/regression/database-overview/database-overview.e2e.ts index a0338ae5ae..8f6288107c 100644 --- a/tests/e2e/tests/regression/database-overview/database-overview.e2e.ts +++ b/tests/e2e/tests/regression/database-overview/database-overview.e2e.ts @@ -1,4 +1,4 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage, @@ -7,11 +7,13 @@ import { import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keys: string[]; @@ -19,12 +21,12 @@ fixture `Database overview` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can connect to DB and see breadcrumbs at the top of the application', async t => { // Create new keys diff --git a/tests/e2e/tests/regression/database-overview/overview.e2e.ts b/tests/e2e/tests/regression/database-overview/overview.e2e.ts index 20ad1e6c29..98458bd988 100644 --- a/tests/e2e/tests/regression/database-overview/overview.e2e.ts +++ b/tests/e2e/tests/regression/database-overview/overview.e2e.ts @@ -1,19 +1,20 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddRECloudDatabase, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, cloudDatabaseConfig } from '../../../helpers/conf'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); fixture `Overview` .meta({ type: 'regression', rte: rte.reCloud }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); + await databaseHelper.acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); }) .afterEach(async() => { // Delete database - await deleteDatabase(cloudDatabaseConfig.databaseName); + await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName); }); test('Verify that user can see not available metrics from Overview in tooltip with the text " is/are not available"', async t => { // Verify that CPU parameter is not displayed in Overview diff --git a/tests/e2e/tests/regression/database/database-list-search.e2e.ts b/tests/e2e/tests/regression/database/database-list-search.e2e.ts index 5092c6d392..e723081670 100644 --- a/tests/e2e/tests/regression/database/database-list-search.e2e.ts +++ b/tests/e2e/tests/regression/database/database-list-search.e2e.ts @@ -1,17 +1,13 @@ -import { acceptLicenseTerms } from '../../../helpers/database'; -import { - addNewStandaloneDatabasesApi, - deleteStandaloneDatabasesApi, - discoverSentinelDatabaseApi, - addNewOSSClusterDatabaseApi, - deleteOSSClusterDatabaseApi, - deleteAllDatabasesByConnectionTypeApi -} from '../../../helpers/api/api-database'; +import { DatabaseHelper } from '../../../helpers/database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { MyRedisDatabasePage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config, ossSentinelConfig, ossClusterConfig } from '../../../helpers/conf'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const databasesForSearch = [ { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testSearch' }, { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testSecondSearch' }, @@ -30,18 +26,18 @@ fixture `Database list search` .page(commonUrl) .beforeEach(async() => { // Add new databases using API - await acceptLicenseTerms(); - await addNewStandaloneDatabasesApi(databasesForAdding); - await addNewOSSClusterDatabaseApi(ossClusterConfig); - await discoverSentinelDatabaseApi(ossSentinelConfig, 1); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding); + await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig, 1); // Reload Page await myRedisDatabasePage.reloadPage(); }) .afterEach(async() => { // Clear and delete databases - await deleteStandaloneDatabasesApi(databasesForAdding); - await deleteOSSClusterDatabaseApi(ossClusterConfig); - await deleteAllDatabasesByConnectionTypeApi('SENTINEL'); + await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL'); }); test('Verify DB list search', async t => { const searchedDBHostInvalid = 'invalid'; diff --git a/tests/e2e/tests/regression/database/database-sorting.e2e.ts b/tests/e2e/tests/regression/database/database-sorting.e2e.ts index 85d8d3932e..11fe2e1d2d 100644 --- a/tests/e2e/tests/regression/database/database-sorting.e2e.ts +++ b/tests/e2e/tests/regression/database/database-sorting.e2e.ts @@ -1,10 +1,5 @@ -import { acceptLicenseTerms, clickOnEditDatabaseByName } from '../../../helpers/database'; -import { - discoverSentinelDatabaseApi, - addNewOSSClusterDatabaseApi, - addNewStandaloneDatabaseApi, - deleteAllDatabasesApi, deleteAllDatabasesByConnectionTypeApi -} from '../../../helpers/api/api-database'; +import { DatabaseHelper } from '../../../helpers/database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { @@ -16,6 +11,9 @@ import { const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const databases = [ { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: ossStandaloneConfig.databaseName }, { host: ossClusterConfig.ossClusterHost, port: ossClusterConfig.ossClusterPort, databaseName: ossClusterConfig.ossClusterDatabaseName }, @@ -38,20 +36,20 @@ fixture `Remember database sorting` .page(commonUrl) .beforeEach(async() => { // Delete all existing databases - await deleteAllDatabasesApi(); + await databaseAPIRequests.deleteAllDatabasesApi(); // Add new databases using API - await acceptLicenseTerms(); - await addNewStandaloneDatabaseApi(ossStandaloneConfig); - await addNewOSSClusterDatabaseApi(ossClusterConfig); - await discoverSentinelDatabaseApi(ossSentinelConfig, 1); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig, 1); // Reload Page await browserPage.reloadPage(); }) .afterEach(async() => { // Clear and delete databases - await deleteAllDatabasesByConnectionTypeApi('STANDALONE'); - await deleteAllDatabasesByConnectionTypeApi('CLUSTER'); - await deleteAllDatabasesByConnectionTypeApi('SENTINEL'); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('STANDALONE'); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('CLUSTER'); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL'); }); test('Verify that sorting on the list of databases saved when database opened', async t => { // Sort by Connection Type @@ -81,7 +79,7 @@ test('Verify that user has the same sorting if db name is changed', async t => { actualDatabaseList = await myRedisDatabasePage.getAllDatabases(); await myRedisDatabasePage.compareDatabases(actualDatabaseList, await sortList()); // Change DB name inside of sorted list - await clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); + await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); await t.click(myRedisDatabasePage.editAliasButton); await t.typeText(myRedisDatabasePage.aliasInput, newDBName, { replace: true, paste: true }); await t.pressKey('enter'); diff --git a/tests/e2e/tests/regression/database/edit-db.e2e.ts b/tests/e2e/tests/regression/database/edit-db.e2e.ts index 7ceff4921a..de939be8a9 100644 --- a/tests/e2e/tests/regression/database/edit-db.e2e.ts +++ b/tests/e2e/tests/regression/database/edit-db.e2e.ts @@ -1,4 +1,4 @@ -import { acceptLicenseTermsAndAddDatabaseApi, clickOnEditDatabaseByName, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, @@ -7,31 +7,33 @@ import { } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; import { Common } from '../../../helpers/common'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); -const database = Object.assign({}, ossStandaloneConfig); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); +const database = Object.assign({}, ossStandaloneConfig); const previousDatabaseName = Common.generateWord(20); const newDatabaseName = Common.generateWord(20); database.databaseName = previousDatabaseName; const keyName = Common.generateWord(10); -fixture`List of Databases` +fixture `List of Databases` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(database, database.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(database); }); test .after(async() => { // Delete database - await deleteDatabase(newDatabaseName); + await databaseHelper.deleteDatabase(newDatabaseName); })('Verify that user can edit DB alias of Standalone DB', async t => { await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); // Edit alias of added database - await clickOnEditDatabaseByName(database.databaseName); + await databaseHelper.clickOnEditDatabaseByName(database.databaseName); // Verify that timeout input is displayed for edit db window with default value when it wasn't specified await t.expect(myRedisDatabasePage.AddRedisDatabase.timeoutInput.value).eql('30', 'Timeout is not defaulted to 30'); @@ -47,17 +49,17 @@ test test .meta({ env: env.desktop }) .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async t => { // Clear and delete database await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); - await clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); + await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); await t.typeText(myRedisDatabasePage.AddRedisDatabase.portInput, ossStandaloneConfig.port, { replace: true, paste: true }); await t.click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that context for previous database not saved after editing port/username/password/certificates/SSH', async t => { const command = 'HSET'; @@ -69,7 +71,7 @@ test await t.pressKey('enter'); await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); // Edit port of added database - await clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); + await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); await t.typeText(myRedisDatabasePage.AddRedisDatabase.portInput, ossStandaloneBigConfig.port, { replace: true, paste: true }); await t.click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); diff --git a/tests/e2e/tests/regression/database/github.e2e.ts b/tests/e2e/tests/regression/database/github.e2e.ts index 8a71eb4243..f3795c6c38 100644 --- a/tests/e2e/tests/regression/database/github.e2e.ts +++ b/tests/e2e/tests/regression/database/github.e2e.ts @@ -1,25 +1,27 @@ // import {ClientFunction} from 'testcafe'; import {rte, env} from '../../../helpers/constants'; -import { acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import {MyRedisDatabasePage} from '../../../pageObjects'; import {commonUrl, ossStandaloneConfig} from '../../../helpers/conf'; -import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); // const getPageUrl = ClientFunction(() => window.location.href); fixture `Github functionality` .meta({ type: 'regression' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); - await addNewStandaloneDatabaseApi(ossStandaloneConfig); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig); // Reload Page await myRedisDatabasePage.reloadPage(); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .meta({ rte: rte.standalone, env: env.web })('Verify that user can work with Github link in the application', async t => { diff --git a/tests/e2e/tests/regression/database/logical-databases.e2e.ts b/tests/e2e/tests/regression/database/logical-databases.e2e.ts index d14559d880..5941b14f5c 100644 --- a/tests/e2e/tests/regression/database/logical-databases.e2e.ts +++ b/tests/e2e/tests/regression/database/logical-databases.e2e.ts @@ -1,21 +1,23 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Logical databases` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that if user enters any index of the logical database that does not exist in the database, he can see Redis error "ERR DB index is out of range" and cannot proceed', async t => { const index = '0'; diff --git a/tests/e2e/tests/regression/database/redisstack.e2e.ts b/tests/e2e/tests/regression/database/redisstack.e2e.ts index 2c0b7fcb23..f702162b28 100644 --- a/tests/e2e/tests/regression/database/redisstack.e2e.ts +++ b/tests/e2e/tests/regression/database/redisstack.e2e.ts @@ -1,11 +1,14 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const moduleNameList = ['RediSearch', 'RedisGraph', 'RedisBloom', 'RedisJSON', 'RedisTimeSeries']; fixture `Redis Stack` @@ -13,14 +16,14 @@ fixture `Redis Stack` .page(commonUrl) .beforeEach(async() => { // Add new databases using API - await acceptLicenseTerms(); - await addNewStandaloneDatabaseApi(ossStandaloneConfig); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig); // Reload Page await browserPage.reloadPage(); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see module list Redis Stack icon hovering (without Redis Stack text)', async t => { // Verify that user can see Redis Stack icon when Redis Stack DB is added in the application diff --git a/tests/e2e/tests/regression/insights/feature-flag.e2e.ts b/tests/e2e/tests/regression/insights/feature-flag.e2e.ts index 681da732e1..e887522e71 100644 --- a/tests/e2e/tests/regression/insights/feature-flag.e2e.ts +++ b/tests/e2e/tests/regression/insights/feature-flag.e2e.ts @@ -1,12 +1,9 @@ import * as path from 'path'; import { BrowserPage, MyRedisDatabasePage, SettingsPage } from '../../../pageObjects'; import { RecommendationIds, rte, env } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../helpers/conf'; -import { - addNewStandaloneDatabaseApi, - deleteStandaloneDatabaseApi -} from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { deleteRowsFromTableInDB, getColumnValueFromTableInDB } from '../../../helpers/database-scripts'; import { modifyFeaturesConfigJson, refreshFeaturesTestData, updateControlNumber } from '../../../helpers/insights'; import { Common } from '../../../helpers/common'; @@ -14,6 +11,8 @@ import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const settingsPage = new SettingsPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const featuresConfigTable = 'features_config'; const redisVersionRecom = RecommendationIds.redisVersion; @@ -32,12 +31,12 @@ fixture `Feature flag` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config, ossStandaloneV5Config.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config); await refreshFeaturesTestData(); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); await refreshFeaturesTestData(); }); test('Verify that default config applied when remote config version is lower', async t => { @@ -60,8 +59,8 @@ test('Verify that invaid remote config not applied even if its version is higher }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - await addNewStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneV5Config); await refreshFeaturesTestData(); }) .after(async t => { @@ -69,8 +68,8 @@ test await t.click(browserPage.NavigationPanel.settingsButton); await settingsPage.changeAnalyticsSwitcher(true); // Delete databases connections - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); // Update remote config .json to default await refreshFeaturesTestData(); })('Verify that valid remote config applied with version higher than in the default config', async t => { @@ -129,10 +128,10 @@ test test .meta({ env: env.desktop }) .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config, ossStandaloneV5Config.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config); }) .after(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); // Update remote config .json to default await modifyFeaturesConfigJson(pathes.defaultRemote); // Clear features config table diff --git a/tests/e2e/tests/regression/insights/live-recommendations.e2e.ts b/tests/e2e/tests/regression/insights/live-recommendations.e2e.ts index bf43433c69..1096486f6d 100644 --- a/tests/e2e/tests/regression/insights/live-recommendations.e2e.ts +++ b/tests/e2e/tests/regression/insights/live-recommendations.e2e.ts @@ -1,14 +1,9 @@ import * as path from 'path'; import { BrowserPage, MemoryEfficiencyPage, MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { RecommendationIds, rte } from '../../../helpers/constants'; -import { acceptLicenseTerms, acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../helpers/conf'; -import { - addNewStandaloneDatabaseApi, - addNewStandaloneDatabasesApi, - deleteStandaloneDatabaseApi, - deleteStandaloneDatabasesApi -} from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; import { Telemetry } from '../../../helpers/telemetry'; import { RecommendationsActions } from '../../../common-actions/recommendations-actions'; @@ -20,6 +15,8 @@ const workbenchPage = new WorkbenchPage(); const telemetry = new Telemetry(); const memoryEfficiencyPage = new MemoryEfficiencyPage(); const recommendationsActions = new RecommendationsActions(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const databasesForAdding = [ { host: ossStandaloneV5Config.host, port: ossStandaloneV5Config.port, databaseName: ossStandaloneV5Config.databaseName }, @@ -46,27 +43,27 @@ fixture `Live Recommendations` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); await refreshFeaturesTestData(); await modifyFeaturesConfigJson(featuresConfig); await updateControlNumber(47.2); - await addNewStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig); await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); }) .afterEach(async() => { await refreshFeaturesTestData(); // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .before(async() => { // Add new databases using API - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); await refreshFeaturesTestData(); await modifyFeaturesConfigJson(featuresConfig); await updateControlNumber(47.2); - await addNewStandaloneDatabasesApi(databasesForAdding); + await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding); // Reload Page await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName); @@ -77,7 +74,7 @@ test await refreshFeaturesTestData(); await browserPage.OverviewPanel.changeDbIndex(0); await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabasesApi(databasesForAdding); + await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding); })('Verify Insights panel Recommendations displaying', async t => { await browserPage.InsightsPanel.toggleInsightsPanel(true); // Verify that "Welcome to recommendations" panel displayed when there are no recommendations @@ -108,16 +105,16 @@ test test .requestHooks(logger) .before(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); await refreshFeaturesTestData(); await modifyFeaturesConfigJson(featuresConfig); await updateControlNumber(47.2); - await addNewStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneV5Config); await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName); }).after(async() => { await refreshFeaturesTestData(); - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); })('Verify that user can upvote recommendations', async() => { const notUsefulVoteOption = 'not useful'; const usefulVoteOption = 'useful'; @@ -188,15 +185,15 @@ test('Verify that user can snooze recommendation', async t => { }); test .before(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); await refreshFeaturesTestData(); await modifyFeaturesConfigJson(featuresConfig); await updateControlNumber(47.2); - await addNewStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneV5Config); await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName); }).after(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); await refreshFeaturesTestData(); })('Verify that recommendations from database analysis are displayed in Insight panel above live recommendations', async t => { const redisVersionRecomSelector = browserPage.InsightsPanel.getRecommendationByName(redisVersionRecom); @@ -237,7 +234,7 @@ test .after(async() => { await refreshFeaturesTestData(); await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabasesApi(databasesForAdding); + await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding); })('Verify that key name is displayed for Insights and DA recommendations', async t => { const cliCommand = `JSON.SET ${keyName} $ '{ "model": "Hyperion", "brand": "Velorim"}'`; await browserPage.Cli.sendCommandInCli(cliCommand); diff --git a/tests/e2e/tests/regression/monitor/monitor.e2e.ts b/tests/e2e/tests/regression/monitor/monitor.e2e.ts index 7a43c6608c..4c28c2afd2 100644 --- a/tests/e2e/tests/regression/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/regression/monitor/monitor.e2e.ts @@ -1,5 +1,5 @@ import { Chance } from 'chance'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, SettingsPage, @@ -13,7 +13,7 @@ import { ossStandaloneNoPermissionsConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi, deleteStandaloneDatabasesApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { WorkbenchActions } from '../../../common-actions/workbench-actions'; const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -22,16 +22,18 @@ const browserPage = new BrowserPage(); const chance = new Chance(); const workbenchPage = new WorkbenchPage(); const workbenchActions = new WorkbenchActions(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Monitor` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify Monitor refresh/stop', async t => { // Run monitor @@ -87,7 +89,7 @@ test('Verify Monitor refresh/stop', async t => { }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); await t.click(myRedisDatabasePage.NavigationPanel.settingsButton); await t.click(settingsPage.accordionAdvancedSettings); await settingsPage.changeKeysToScanValue('20000000'); @@ -99,7 +101,7 @@ test await t.click(settingsPage.accordionAdvancedSettings); await settingsPage.changeKeysToScanValue('10000'); // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can see monitor results in high DB load', async t => { // Run monitor await browserPage.Profiler.startMonitor(); @@ -116,14 +118,14 @@ test // Skipped due to redis issue https://redislabs.atlassian.net/browse/RI-4111 test.skip .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await browserPage.Cli.sendCommandInCli('acl setuser noperm nopass on +@all ~* -monitor -client'); // Check command result in CLI await t.click(browserPage.Cli.cliExpandButton); await t.expect(browserPage.Cli.cliOutputResponseSuccess.textContent).eql('"OK"', 'Command from autocomplete was not found & executed'); await t.click(browserPage.Cli.cliCollapseButton); await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); - await addNewStandaloneDatabaseApi(ossStandaloneNoPermissionsConfig); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneNoPermissionsConfig); await browserPage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneNoPermissionsConfig.databaseName); }) @@ -133,7 +135,7 @@ test.skip await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await browserPage.Cli.sendCommandInCli('acl DELUSER noperm'); // Delete database - await deleteStandaloneDatabasesApi([ossStandaloneConfig, ossStandaloneNoPermissionsConfig]); + await databaseAPIRequests.deleteStandaloneDatabasesApi([ossStandaloneConfig, ossStandaloneNoPermissionsConfig]); })('Verify that if user doesn\'t have permissions to run monitor, user can see error message', async t => { const command = 'CLIENT LIST'; // Expand the Profiler diff --git a/tests/e2e/tests/regression/monitor/save-commands.e2e.ts b/tests/e2e/tests/regression/monitor/save-commands.e2e.ts index 1a2f3fd999..b3e2b016a6 100644 --- a/tests/e2e/tests/regression/monitor/save-commands.e2e.ts +++ b/tests/e2e/tests/regression/monitor/save-commands.e2e.ts @@ -1,26 +1,26 @@ import * as fs from 'fs'; import * as os from 'os'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const tempDir = os.tmpdir(); fixture `Save commands` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that when clicks on “Reset Profiler” button he brought back to Profiler home screen', async t => { // Start Monitor without Save logs diff --git a/tests/e2e/tests/regression/pub-sub/debug-mode.e2e.ts b/tests/e2e/tests/regression/pub-sub/debug-mode.e2e.ts index d00b747072..16b96b86fc 100644 --- a/tests/e2e/tests/regression/pub-sub/debug-mode.e2e.ts +++ b/tests/e2e/tests/regression/pub-sub/debug-mode.e2e.ts @@ -1,18 +1,20 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, PubSubPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; import { verifyMessageDisplayingInPubSub } from '../../../helpers/pub-sub'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const pubSubPage = new PubSubPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `PubSub debug mode` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to PubSub page and subscribe to channel await t.click(myRedisDatabasePage.NavigationPanel.pubSubButton); await t.click(pubSubPage.subscribeButton); @@ -22,7 +24,7 @@ fixture `PubSub debug mode` await pubSubPage.Cli.sendCommandInCli('10 publish channel third'); }) .afterEach(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test diff --git a/tests/e2e/tests/regression/pub-sub/pub-sub-oss-cluster-7.ts b/tests/e2e/tests/regression/pub-sub/pub-sub-oss-cluster-7.ts index e49d9ac886..03ed9282ac 100644 --- a/tests/e2e/tests/regression/pub-sub/pub-sub-oss-cluster-7.ts +++ b/tests/e2e/tests/regression/pub-sub/pub-sub-oss-cluster-7.ts @@ -1,17 +1,14 @@ -import { - acceptLicenseTerms -} from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, PubSubPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig, ossClusterConfig } from '../../../helpers/conf'; import { rte, env } from '../../../helpers/constants'; import { verifyMessageDisplayingInPubSub } from '../../../helpers/pub-sub'; -import { - addNewOSSClusterDatabaseApi, addNewStandaloneDatabaseApi, - deleteOSSClusterDatabaseApi, deleteStandaloneDatabaseApi -} from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const pubSubPage = new PubSubPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `PubSub OSS Cluster 7 tests` .meta({ env: env.web, type: 'regression' }) @@ -19,14 +16,14 @@ fixture `PubSub OSS Cluster 7 tests` test .before(async t => { - await acceptLicenseTerms(); - await addNewOSSClusterDatabaseApi(ossClusterConfig); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig); await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossClusterConfig.ossClusterDatabaseName); await t.click(myRedisDatabasePage.NavigationPanel.pubSubButton); }) .after(async() => { - await deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); }) .meta({ rte: rte.ossCluster })('Verify that SPUBLISH message is displayed for OSS Cluster 7 database', async t => { await t.expect(pubSubPage.ossClusterEmptyMessage.exists).ok('SPUBLISH message not displayed'); @@ -41,14 +38,14 @@ test }); test .before(async t => { - await acceptLicenseTerms(); - await addNewStandaloneDatabaseApi(ossStandaloneConfig); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig); await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await t.click(myRedisDatabasePage.NavigationPanel.pubSubButton); }) .after(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }) .meta({ rte: rte.standalone })('Verify that SPUBLISH message is not displayed for other databases expect OSS Cluster 7', async t => { await t.expect(pubSubPage.ossClusterEmptyMessage.exists).notOk('No SPUBLISH message still displayed'); diff --git a/tests/e2e/tests/regression/shortcuts/shortcuts.e2e.ts b/tests/e2e/tests/regression/shortcuts/shortcuts.e2e.ts index ab376b9575..2f2e18215b 100644 --- a/tests/e2e/tests/regression/shortcuts/shortcuts.e2e.ts +++ b/tests/e2e/tests/regression/shortcuts/shortcuts.e2e.ts @@ -1,17 +1,18 @@ // import { ClientFunction } from 'testcafe'; import { rte, env } from '../../../helpers/constants'; -import { acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl } from '../../../helpers/conf'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); // const getPageUrl = ClientFunction(() => window.location.href); fixture `Shortcuts` .meta({ type: 'regression', rte: rte.none }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); test .meta({ env: env.web })('Verify that user can see a summary of Shortcuts by clicking "Keyboard Shortcuts" button in Help Center', async t => { diff --git a/tests/e2e/tests/regression/tree-view/tree-view.e2e.ts b/tests/e2e/tests/regression/tree-view/tree-view.e2e.ts index 8e842e76af..df8c055b09 100644 --- a/tests/e2e/tests/regression/tree-view/tree-view.e2e.ts +++ b/tests/e2e/tests/regression/tree-view/tree-view.e2e.ts @@ -1,4 +1,4 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, @@ -6,28 +6,30 @@ import { ossStandaloneRedisearch } from '../../../helpers/conf'; import { KeyTypesTexts, rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Tree view verifications` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); }) .after(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Verify that user can see message "No keys to display." when there are no keys in the database', async t => { const message = 'No keys to display.Use Workbench Guides and Tutorials to quickly load the data.'; diff --git a/tests/e2e/tests/regression/workbench/autocomplete.e2e.ts b/tests/e2e/tests/regression/workbench/autocomplete.e2e.ts index 38331b544a..0b0c9e2b5d 100644 --- a/tests/e2e/tests/regression/workbench/autocomplete.e2e.ts +++ b/tests/e2e/tests/regression/workbench/autocomplete.e2e.ts @@ -1,23 +1,25 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Autocomplete for entered commands` .meta({type: 'regression', rte: rte.standalone}) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can open the "read more" about the command by clicking on the ">" icon or "ctrl+space"', async t => { const command = 'HSET'; diff --git a/tests/e2e/tests/regression/workbench/autoexecute-button.e2e.ts b/tests/e2e/tests/regression/workbench/autoexecute-button.e2e.ts index 15513c7954..37db89292e 100644 --- a/tests/e2e/tests/regression/workbench/autoexecute-button.e2e.ts +++ b/tests/e2e/tests/regression/workbench/autoexecute-button.e2e.ts @@ -1,22 +1,24 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage } from '../../../pageObjects'; import { env, rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Workbench Auto-Execute button` .meta({ type: 'regression', rte: rte.standalone, env: env.web }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Clear and delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); // Test is skipped until Enablement area will be updated with auto-execute buttons test.skip('Verify that when user clicks on auto-execute button, command is run', async t => { diff --git a/tests/e2e/tests/regression/workbench/command-results.e2e.ts b/tests/e2e/tests/regression/workbench/command-results.e2e.ts index 96c841c6d5..c76bb4ee7d 100644 --- a/tests/e2e/tests/regression/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/regression/workbench/command-results.e2e.ts @@ -1,17 +1,16 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneRedisearch -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; import { WorkbenchActions } from '../../../common-actions/workbench-actions'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const workBenchActions = new WorkbenchActions(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const indexName = Common.generateWord(5); const commandsForIndex = [ @@ -24,7 +23,7 @@ fixture `Command results at Workbench` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Add index and data await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex); @@ -33,7 +32,7 @@ fixture `Command results at Workbench` // Drop index and database await t.switchToMainWindow(); await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); test .meta({ env: env.web })('Verify that user can switches between Table and Text for FT.INFO and see results corresponding to their views', async t => { @@ -119,12 +118,12 @@ test('Big output in workbench is visible in virtualized table', async t => { }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .after(async t => { await t.switchToMainWindow(); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Verify that user can see the client List visualization available for all users', async t => { const command = 'CLIENT LIST'; // Send command in workbench to view client list diff --git a/tests/e2e/tests/regression/workbench/context.e2e.ts b/tests/e2e/tests/regression/workbench/context.e2e.ts index 91dbb0aa91..595a1caf81 100644 --- a/tests/e2e/tests/regression/workbench/context.e2e.ts +++ b/tests/e2e/tests/regression/workbench/context.e2e.ts @@ -1,11 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const speed = 0.4; @@ -13,13 +15,13 @@ fixture `Workbench Context` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see saved CLI state when navigates away to any other page', async t => { // Expand CLI and navigate to Browser diff --git a/tests/e2e/tests/regression/workbench/cypher.e2e.ts b/tests/e2e/tests/regression/workbench/cypher.e2e.ts index 4e4c17d888..21cd442382 100644 --- a/tests/e2e/tests/regression/workbench/cypher.e2e.ts +++ b/tests/e2e/tests/regression/workbench/cypher.e2e.ts @@ -1,11 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const command = 'GRAPH.QUERY graph'; @@ -13,13 +15,13 @@ fixture `Cypher syntax at Workbench` .meta({type: 'regression', rte: rte.standalone}) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Drop database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see popover “Use Cypher Syntax” when cursor is inside the query argument double/single quotes in the GRAPH command', async t => { // Type command and put the cursor inside diff --git a/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts index 79b6bde9ab..ae8d9e0abd 100644 --- a/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts @@ -1,23 +1,25 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Default scripts area at Workbench` .meta({type: 'regression', rte: rte.standalone}) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }) test('Verify that user can expand/collapse the enablement area', async t => { // Hover over Enablement area diff --git a/tests/e2e/tests/regression/workbench/editor-cleanup.e2e.ts b/tests/e2e/tests/regression/workbench/editor-cleanup.e2e.ts index 1047ff7a21..27c1256379 100644 --- a/tests/e2e/tests/regression/workbench/editor-cleanup.e2e.ts +++ b/tests/e2e/tests/regression/workbench/editor-cleanup.e2e.ts @@ -1,12 +1,14 @@ -import { acceptLicenseTerms, acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage, SettingsPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { addNewStandaloneDatabasesApi, deleteStandaloneDatabaseApi, deleteStandaloneDatabasesApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const settingsPage = new SettingsPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const commandToSend = 'info server'; const databasesForAdding = [ @@ -18,11 +20,11 @@ fixture `Workbench Editor Cleanup` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Disabled Editor Cleanup toggle behavior', async t => { // Go to Settings page @@ -52,15 +54,15 @@ test('Enabled Editor Cleanup toggle behavior', async t => { test .before(async() => { // Add new databases using API - await acceptLicenseTerms(); - await addNewStandaloneDatabasesApi(databasesForAdding); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding); // Reload Page await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName); }) .after(async() => { // Clear and delete database - await deleteStandaloneDatabasesApi(databasesForAdding); + await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding); })('Editor Cleanup settings', async t => { // Go to Settings page await t.click(myRedisDatabasePage.NavigationPanel.settingsButton); diff --git a/tests/e2e/tests/regression/workbench/empty-command-history.e2e.ts b/tests/e2e/tests/regression/workbench/empty-command-history.e2e.ts index 646b95d1a2..03c3ed6708 100644 --- a/tests/e2e/tests/regression/workbench/empty-command-history.e2e.ts +++ b/tests/e2e/tests/regression/workbench/empty-command-history.e2e.ts @@ -1,24 +1,26 @@ import { Selector } from 'testcafe'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Empty command history in Workbench` .meta({type: 'regression'}) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .meta({ rte: rte.standalone })('Verify that user can see placeholder text in Workbench history if no commands have not been run yet', async t => { diff --git a/tests/e2e/tests/regression/workbench/group-mode.e2e.ts b/tests/e2e/tests/regression/workbench/group-mode.e2e.ts index d8a4479acf..1b84868c44 100644 --- a/tests/e2e/tests/regression/workbench/group-mode.e2e.ts +++ b/tests/e2e/tests/regression/workbench/group-mode.e2e.ts @@ -1,12 +1,15 @@ import { Selector } from 'testcafe'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const counter = 7; const command = 'RANDOMKEY'; const commands = ['set key test', 'get key', 'del key']; @@ -18,13 +21,13 @@ fixture `Workbench Group Mode` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test('Verify that user can run the commands from the Editor in the group mode', async t => { await t.click(workbenchPage.groupMode); diff --git a/tests/e2e/tests/regression/workbench/history-of-results.e2e.ts b/tests/e2e/tests/regression/workbench/history-of-results.e2e.ts index bcc8ff9046..3a63cda9a6 100644 --- a/tests/e2e/tests/regression/workbench/history-of-results.e2e.ts +++ b/tests/e2e/tests/regression/workbench/history-of-results.e2e.ts @@ -1,13 +1,15 @@ import { getRandomParagraph } from '../../../helpers/keys'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const oneMinuteTimeout = 60000; let keyName = Common.generateWord(10); @@ -17,14 +19,14 @@ fixture `History of results at Workbench` .meta({ type: 'regression' }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Clear and delete database await workbenchPage.Cli.sendCommandInCli(`DEL ${keyName}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .meta({ rte: rte.standalone })('Verify that user can see original date and time of command execution in Workbench history after the page update', async t => { @@ -42,7 +44,7 @@ test.skip .meta({ rte: rte.standalone }) .after(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that if command result is more than 1 MB and user refreshes the page, the message "Results have been deleted since they exceed 1 MB. Re-run the command to see new results." is displayed', async t => { const commandToSend = 'set key'; const commandToGet = 'get key'; diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index f2122892ef..f84765f18d 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -1,18 +1,19 @@ import * as path from 'path'; import { t } from 'testcafe'; import { rte } from '../../../helpers/constants'; -import { - acceptLicenseTermsAndAddDatabaseApi -} from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } 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 browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const zipFolderName = 'customTutorials'; const folderPath = path.join('..', 'test-data', 'upload-tutorials', zipFolderName); const folder1 = 'folder-1'; @@ -33,18 +34,18 @@ fixture `Upload custom tutorials` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.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 .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); tutorialName = `${zipFolderName}${Common.generateWord(5)}`; zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`); @@ -54,7 +55,7 @@ test .after(async() => { // Delete zip file await Common.deleteFileFromFolder(zipFilePath); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.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'; @@ -143,7 +144,7 @@ test('Verify that user can upload tutorial with URL with manifest.json', async t // https://redislabs.atlassian.net/browse/RI-4352 test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); tutorialName = `${zipFolderName}${Common.generateWord(5)}`; zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`); @@ -158,7 +159,7 @@ test await workbenchPage.deleteTutorialByName(tutorialName); await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).exists) .notOk(`${tutorialName} tutorial is not deleted`); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.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']; diff --git a/tests/e2e/tests/regression/workbench/raw-mode.e2e.ts b/tests/e2e/tests/regression/workbench/raw-mode.e2e.ts index 45aa392a1c..f39ed1f775 100644 --- a/tests/e2e/tests/regression/workbench/raw-mode.e2e.ts +++ b/tests/e2e/tests/regression/workbench/raw-mode.e2e.ts @@ -1,13 +1,15 @@ -import { acceptLicenseTerms, acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../helpers/conf'; -import { addNewStandaloneDatabasesApi, deleteStandaloneDatabaseApi, deleteStandaloneDatabasesApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const keyName = Common.generateWord(10); const indexName = Common.generateWord(5); @@ -26,7 +28,7 @@ fixture `Workbench Raw mode` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) @@ -34,7 +36,7 @@ fixture `Workbench Raw mode` // Clear and delete database await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Use raw mode for Workbech result', async t => { // Send commands @@ -55,8 +57,8 @@ test('Use raw mode for Workbech result', async t => { test .before(async t => { // Add new databases using API - await acceptLicenseTerms(); - await addNewStandaloneDatabasesApi(databasesForAdding); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding); // Reload Page await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName); @@ -65,7 +67,7 @@ test }) .after(async() => { // Clear and delete database - await deleteStandaloneDatabasesApi(databasesForAdding); + await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding); })('Save Raw mode state', async t => { // Send command in raw mode await t.click(workbenchPage.rawModeBtn); @@ -88,7 +90,7 @@ test }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) @@ -96,7 +98,7 @@ test // Drop index, documents and database await t.switchToMainWindow(); await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Display Raw mode for plugins', async t => { const commandsForSend = [ `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`, diff --git a/tests/e2e/tests/regression/workbench/redis-stack-commands.e2e.ts b/tests/e2e/tests/regression/workbench/redis-stack-commands.e2e.ts index 6f19453b8b..327c0454ab 100644 --- a/tests/e2e/tests/regression/workbench/redis-stack-commands.e2e.ts +++ b/tests/e2e/tests/regression/workbench/redis-stack-commands.e2e.ts @@ -1,29 +1,29 @@ import { t } from 'testcafe'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const keyNameGraph = 'bikes_graph'; fixture `Redis Stack command in Workbench` .meta({type: 'regression', rte: rte.standalone}) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Drop key and database await t.switchToMainWindow(); await workbenchPage.sendCommandInWorkbench(`GRAPH.DELETE ${keyNameGraph}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); //skipped due the inaccessibility of the iframe test.skip diff --git a/tests/e2e/tests/regression/workbench/redisearch-module-not-available.e2e.ts b/tests/e2e/tests/regression/workbench/redisearch-module-not-available.e2e.ts index 29d162fee7..24f0aee100 100644 --- a/tests/e2e/tests/regression/workbench/redisearch-module-not-available.e2e.ts +++ b/tests/e2e/tests/regression/workbench/redisearch-module-not-available.e2e.ts @@ -1,12 +1,14 @@ import { ClientFunction } from 'testcafe'; import { env, rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneV5Config } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const commandForSend = 'FT._LIST'; const getPageUrl = ClientFunction(() => window.location.href); @@ -15,13 +17,13 @@ fixture `Redisearch module not available` .meta({type: 'regression'}) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config, ossStandaloneV5Config.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); }); // Skipped as outdated after implementing RI-4230 test.skip @@ -41,11 +43,11 @@ test.skip test .meta({ env: env.web, rte: rte.standalone }) .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config, ossStandaloneV5Config.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config); await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .after(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneV5Config); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); })('Verify that user can see options on what can be done to work with capabilities in Workbench for docker', async t => { const commandJSON = 'JSON.ARRAPPEND key value'; const commandFT = 'FT.LIST'; diff --git a/tests/e2e/tests/regression/workbench/scripting-area.e2e.ts b/tests/e2e/tests/regression/workbench/scripting-area.e2e.ts index c285160ad2..1ef82d61a3 100644 --- a/tests/e2e/tests/regression/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/regression/workbench/scripting-area.e2e.ts @@ -1,14 +1,16 @@ import { Selector } from 'testcafe'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage, SettingsPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const settingsPage = new SettingsPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const indexName = Common.generateWord(5); let keyName = Common.generateWord(5); @@ -17,14 +19,14 @@ fixture `Scripting area at Workbench` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Clear and delete database await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can run multiple commands written in multiple lines in Workbench page', async t => { const commandsForSend = [ @@ -53,7 +55,7 @@ test .after(async() => { // Clear and delete database await workbenchPage.Cli.sendCommandInCli(`DEL ${keyName}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can use double slashes (//) wrapped in double quotes and these slashes will not comment out any characters', async t => { keyName = Common.generateWord(10); const commandsForSend = [ @@ -79,7 +81,7 @@ test test .after(async() => { // Clear and delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can see an indication (green triangle) of commands from the left side of the line numbers', async t => { // Open Working with Hashes page await t.click(workbenchPage.documentButtonInQuickGuides); @@ -98,7 +100,7 @@ test .after(async() => { // Clear and delete database await workbenchPage.Cli.sendCommandInCli(`DEL ${keyName}`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can find (using right click) "Run Commands" custom shortcut option in monaco menu and run a command', async t => { keyName = Common.generateWord(10); const command = `HSET ${keyName} field value`; diff --git a/tests/e2e/tests/regression/workbench/workbench-non-auto-guides.e2e.ts b/tests/e2e/tests/regression/workbench/workbench-non-auto-guides.e2e.ts index 2f056dfcbd..192331e409 100644 --- a/tests/e2e/tests/regression/workbench/workbench-non-auto-guides.e2e.ts +++ b/tests/e2e/tests/regression/workbench/workbench-non-auto-guides.e2e.ts @@ -1,13 +1,15 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const counter = 7; const unicodeValue = '山女馬 / 马目 abc 123'; @@ -33,16 +35,16 @@ fixture `Workbench modes to non-auto guides` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench(`set ${keyName} "${keyValue}"`); }) @@ -50,7 +52,7 @@ test // Clear and delete database await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Workbench modes from editor', async t => { const groupCommandResultName = `${counter} Command(s) - ${counter} success, 0 error(s)`; const containerOfCommand = await workbenchPage.getCardContainerByCommand(groupCommandResultName); diff --git a/tests/e2e/tests/regression/workbench/workbench-pipeline.e2e.ts b/tests/e2e/tests/regression/workbench/workbench-pipeline.e2e.ts index 336685a67a..c2d32159b7 100644 --- a/tests/e2e/tests/regression/workbench/workbench-pipeline.e2e.ts +++ b/tests/e2e/tests/regression/workbench/workbench-pipeline.e2e.ts @@ -1,16 +1,18 @@ -import { ClientFunction } from 'testcafe'; +// import { ClientFunction } from 'testcafe'; import { env, rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, SettingsPage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const settingsPage = new SettingsPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); -const getPageUrl = ClientFunction(() => window.location.href); -const externalPageLink = 'https://redis.io/docs/manual/pipelining/'; +// const getPageUrl = ClientFunction(() => window.location.href); +// const externalPageLink = 'https://redis.io/docs/manual/pipelining/'; const pipelineValues = ['-5', '5', '4', '20']; const commandForSend = '100 scan 0 match * count 5000'; @@ -18,14 +20,14 @@ fixture `Workbench Pipeline` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); // Go to Settings page - Pipeline mode await t.click(myRedisDatabasePage.NavigationPanel.settingsButton); await t.click(settingsPage.accordionWorkbenchSettings); }) .afterEach(async() => { // Delete database - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test .meta({ env: env.web })('Verify that user can see the text in settings for pipeline with link', async t => { diff --git a/tests/e2e/tests/regression/workbench/workbench-re-cluster.e2e.ts b/tests/e2e/tests/regression/workbench/workbench-re-cluster.e2e.ts index ef5e1bb7a9..0a52086460 100644 --- a/tests/e2e/tests/regression/workbench/workbench-re-cluster.e2e.ts +++ b/tests/e2e/tests/regression/workbench/workbench-re-cluster.e2e.ts @@ -1,18 +1,14 @@ import { t } from 'testcafe'; import { env, rte } from '../../../helpers/constants'; -import { - acceptLicenseTermsAndAddOSSClusterDatabase, - acceptLicenseTermsAndAddRECloudDatabase, - acceptLicenseTermsAndAddREClusterDatabase, - acceptLicenseTermsAndAddSentinelDatabaseApi, - deleteDatabase -} from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { cloudDatabaseConfig, commonUrl, ossClusterConfig, ossSentinelConfig, redisEnterpriseClusterConfig } from '../../../helpers/conf'; -import { deleteOSSClusterDatabaseApi, deleteAllDatabasesByConnectionTypeApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const commandForSend1 = 'info'; const commandForSend2 = 'FT._LIST'; @@ -45,44 +41,44 @@ fixture `Work with Workbench in all types of databases` test .meta({ rte: rte.reCluster }) .before(async() => { - await acceptLicenseTermsAndAddREClusterDatabase(redisEnterpriseClusterConfig); + await databaseHelper.acceptLicenseTermsAndAddREClusterDatabase(redisEnterpriseClusterConfig); }) .after(async() => { // Delete database - await deleteDatabase(redisEnterpriseClusterConfig.databaseName); + await databaseHelper.deleteDatabase(redisEnterpriseClusterConfig.databaseName); })('Verify that user can run commands in Workbench in RE Cluster DB', async() => { await verifyCommandsInWorkbench(); }); test .meta({ rte: rte.reCloud }) .before(async() => { - await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); + await databaseHelper.acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); }) .after(async() => { // Delete database - await deleteDatabase(cloudDatabaseConfig.databaseName); + await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName); })('Verify that user can run commands in Workbench in RE Cloud DB', async() => { await verifyCommandsInWorkbench(); }); test .meta({ rte: rte.ossCluster }) .before(async() => { - await acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig, ossClusterConfig.ossClusterDatabaseName); + await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig); }) .after(async() => { // Delete database - await deleteOSSClusterDatabaseApi(ossClusterConfig); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); })('Verify that user can run commands in Workbench in OSS Cluster DB', async() => { await verifyCommandsInWorkbench(); }); test .meta({ env: env.web, rte: rte.sentinel }) .before(async() => { - await acceptLicenseTermsAndAddSentinelDatabaseApi(ossSentinelConfig); + await databaseHelper.acceptLicenseTermsAndAddSentinelDatabaseApi(ossSentinelConfig); }) .after(async() => { // Delete database - await deleteAllDatabasesByConnectionTypeApi('SENTINEL'); + await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL'); })('Verify that user can run commands in Workbench in Sentinel Primary Group', async() => { await verifyCommandsInWorkbench(); }); diff --git a/tests/e2e/tests/smoke/browser/add-keys.e2e.ts b/tests/e2e/tests/smoke/browser/add-keys.e2e.ts index ac141073d2..1f6a9e1945 100644 --- a/tests/e2e/tests/smoke/browser/add-keys.e2e.ts +++ b/tests/e2e/tests/smoke/browser/add-keys.e2e.ts @@ -1,10 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { deleteDatabase, acceptTermsAddDatabaseOrConnectToRedisStack } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); @@ -12,12 +15,12 @@ fixture `Add keys` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can add Hash Key', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/smoke/browser/edit-key-name.e2e.ts b/tests/e2e/tests/smoke/browser/edit-key-name.e2e.ts index 7cf9359360..bd24a99482 100644 --- a/tests/e2e/tests/smoke/browser/edit-key-name.e2e.ts +++ b/tests/e2e/tests/smoke/browser/edit-key-name.e2e.ts @@ -1,12 +1,15 @@ import { rte } from '../../../helpers/constants'; -import { deleteDatabase, acceptTermsAddDatabaseOrConnectToRedisStack } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; import { Telemetry } from '../../../helpers/telemetry'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); const telemetry = new Telemetry(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyNameBefore = Common.generateWord(10); let keyNameAfter = Common.generateWord(10); @@ -23,12 +26,12 @@ fixture `Edit Key names verification` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyNameAfter); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .requestHooks(logger)('Verify that user can edit String Key name', async t => { diff --git a/tests/e2e/tests/smoke/browser/edit-key-value.e2e.ts b/tests/e2e/tests/smoke/browser/edit-key-value.e2e.ts index f3041640ca..46f3d69ecc 100644 --- a/tests/e2e/tests/smoke/browser/edit-key-value.e2e.ts +++ b/tests/e2e/tests/smoke/browser/edit-key-value.e2e.ts @@ -1,11 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const keyTTL = '2147476121'; const keyValueBefore = 'ValueBeforeEdit!'; @@ -16,12 +18,12 @@ fixture `Edit Key values verification` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can edit String value', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/smoke/browser/filtering.e2e.ts b/tests/e2e/tests/smoke/browser/filtering.e2e.ts index 70d53597cc..f8970e117b 100644 --- a/tests/e2e/tests/smoke/browser/filtering.e2e.ts +++ b/tests/e2e/tests/smoke/browser/filtering.e2e.ts @@ -1,10 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { deleteDatabase, acceptTermsAddDatabaseOrConnectToRedisStack } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = `KeyForSearch*?[]789${Common.generateWord(10)}`; let keyName2 = Common.generateWord(10); @@ -17,12 +20,12 @@ fixture `Filtering per key name in Browser page` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(`${searchedKeyName}${randomValue}`); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can search per full key name', async t => { randomValue = Common.generateWord(10); @@ -57,7 +60,7 @@ test await browserPage.deleteKeyByName(keyName); await browserPage.deleteKeyByName(keyName2); await browserPage.deleteKeyByName(searchedValueWithEscapedSymbols); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can filter per combined pattern with ?, *, [xy], [^x], [a-z] and escaped special symbols', async t => { keyName = `KeyForSearch${Common.generateWord(10)}`; keyName2 = `KeyForSomething${Common.generateWord(10)}`; diff --git a/tests/e2e/tests/smoke/browser/hash-field.e2e.ts b/tests/e2e/tests/smoke/browser/hash-field.e2e.ts index f8b68abbb8..7df0e4f22a 100644 --- a/tests/e2e/tests/smoke/browser/hash-field.e2e.ts +++ b/tests/e2e/tests/smoke/browser/hash-field.e2e.ts @@ -1,10 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { deleteDatabase, acceptTermsAddDatabaseOrConnectToRedisStack } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -15,12 +18,12 @@ fixture `Hash Key fields verification` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can add field to Hash', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/smoke/browser/json-key.e2e.ts b/tests/e2e/tests/smoke/browser/json-key.e2e.ts index 30979b5c10..6b45c2015a 100644 --- a/tests/e2e/tests/smoke/browser/json-key.e2e.ts +++ b/tests/e2e/tests/smoke/browser/json-key.e2e.ts @@ -1,10 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { deleteDatabase, acceptTermsAddDatabaseOrConnectToRedisStack } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -15,12 +18,12 @@ fixture `JSON Key verification` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can add key with value to any level of JSON structure', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/smoke/browser/list-key.e2e.ts b/tests/e2e/tests/smoke/browser/list-key.e2e.ts index 8073676724..a96dd1189d 100644 --- a/tests/e2e/tests/smoke/browser/list-key.e2e.ts +++ b/tests/e2e/tests/smoke/browser/list-key.e2e.ts @@ -1,10 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { deleteDatabase, acceptTermsAddDatabaseOrConnectToRedisStack } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -16,12 +19,12 @@ fixture `List Key verification` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can select remove List element position: from tail', async t => { keyName = Common.generateWord(10); 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 2a3073368c..18683a30c6 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 @@ -1,10 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { deleteDatabase, acceptTermsAddDatabaseOrConnectToRedisStack } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); let keyNames: string[] = []; @@ -14,12 +17,12 @@ fixture `List of keys verifications` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .after(async() => { @@ -27,7 +30,7 @@ test for(const name of keyNames) { await browserPage.deleteKeyByName(name); } - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can scroll List of Keys in DB', async t => { keyNames = [ `key-${Common.generateWord(10)}`, diff --git a/tests/e2e/tests/smoke/browser/set-key.e2e.ts b/tests/e2e/tests/smoke/browser/set-key.e2e.ts index 373177e588..6b3bc59314 100644 --- a/tests/e2e/tests/smoke/browser/set-key.e2e.ts +++ b/tests/e2e/tests/smoke/browser/set-key.e2e.ts @@ -1,13 +1,13 @@ -import { acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -17,12 +17,12 @@ fixture `Set Key fields verification` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can remove member from Set', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/smoke/browser/set-ttl-for-key.e2e.ts b/tests/e2e/tests/smoke/browser/set-ttl-for-key.e2e.ts index 0da0b9668b..2f748c0263 100644 --- a/tests/e2e/tests/smoke/browser/set-ttl-for-key.e2e.ts +++ b/tests/e2e/tests/smoke/browser/set-ttl-for-key.e2e.ts @@ -1,10 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); @@ -12,12 +15,12 @@ fixture `Set TTL for Key` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can specify TTL for Key', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/smoke/browser/verify-key-details.e2e.ts b/tests/e2e/tests/smoke/browser/verify-key-details.e2e.ts index 811322cbcc..62be2f3fff 100644 --- a/tests/e2e/tests/smoke/browser/verify-key-details.e2e.ts +++ b/tests/e2e/tests/smoke/browser/verify-key-details.e2e.ts @@ -1,10 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -14,12 +17,12 @@ fixture `Key details verification` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see Hash Key details', async t => { keyName = Common.generateWord(10); 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 daef813d50..c87339a6f4 100644 --- a/tests/e2e/tests/smoke/browser/verify-keys-refresh.e2e.ts +++ b/tests/e2e/tests/smoke/browser/verify-keys-refresh.e2e.ts @@ -1,10 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); let newKeyName = Common.generateWord(10); @@ -13,12 +16,12 @@ fixture `Keys refresh functionality` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(newKeyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can refresh Keys', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/smoke/browser/zset-key.e2e.ts b/tests/e2e/tests/smoke/browser/zset-key.e2e.ts index fa94585a99..102e1694bb 100644 --- a/tests/e2e/tests/smoke/browser/zset-key.e2e.ts +++ b/tests/e2e/tests/smoke/browser/zset-key.e2e.ts @@ -1,10 +1,13 @@ import { rte } from '../../../helpers/constants'; -import { acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); const keyTTL = '2147476121'; @@ -15,12 +18,12 @@ fixture `ZSet Key fields verification` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can remove member from ZSet', async t => { keyName = Common.generateWord(10); diff --git a/tests/e2e/tests/smoke/cli/cli-command-helper.e2e.ts b/tests/e2e/tests/smoke/cli/cli-command-helper.e2e.ts index 9a5e3c5ae7..9750aee5c9 100644 --- a/tests/e2e/tests/smoke/cli/cli-command-helper.e2e.ts +++ b/tests/e2e/tests/smoke/cli/cli-command-helper.e2e.ts @@ -1,9 +1,12 @@ import { rte } from '../../../helpers/constants'; -import { acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { BrowserPage } from '../../../pageObjects'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); const COMMAND_APPEND = 'APPEND'; const COMMAND_GROUP_SET = 'Set'; @@ -12,11 +15,11 @@ fixture `CLI Command helper` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can search per command in Command Helper and see relevant results', async t => { const commandForSearch = 'ADD'; diff --git a/tests/e2e/tests/smoke/cli/cli.e2e.ts b/tests/e2e/tests/smoke/cli/cli.e2e.ts index 3dd92db85f..8edc1836bf 100644 --- a/tests/e2e/tests/smoke/cli/cli.e2e.ts +++ b/tests/e2e/tests/smoke/cli/cli.e2e.ts @@ -1,11 +1,14 @@ import { env, rte } from '../../../helpers/constants'; -import { acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let keyName = Common.generateWord(10); @@ -13,16 +16,16 @@ fixture `CLI` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .afterEach(async() => { // Delete database - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test .after(async() => { await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can add data via CLI', async t => { keyName = Common.generateWord(10); // Open CLI diff --git a/tests/e2e/tests/smoke/database/add-db-from-welcome-page.e2e.ts b/tests/e2e/tests/smoke/database/add-db-from-welcome-page.e2e.ts index 8f8d66350e..665133670d 100644 --- a/tests/e2e/tests/smoke/database/add-db-from-welcome-page.e2e.ts +++ b/tests/e2e/tests/smoke/database/add-db-from-welcome-page.e2e.ts @@ -1,12 +1,15 @@ import { ClientFunction } from 'testcafe'; import { env, rte } from '../../../helpers/constants'; -import { acceptLicenseTerms, addNewStandaloneDatabase, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteAllDatabasesApi } from '../../../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browsePage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + const getPageUrl = ClientFunction(() => window.location.href); const sourcePage = 'https://developer.redis.com/create/from-source/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight'; const dockerPage = 'https://developer.redis.com/create/docker/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight'; @@ -17,19 +20,19 @@ fixture `Add database from welcome page` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); - await deleteAllDatabasesApi(); + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.deleteAllDatabasesApi(); // Reload Page await browsePage.reloadPage(); }); test .after(async() => { // Delete database - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseHelper.deleteDatabase(ossStandaloneConfig.databaseName); })('Verify that user can add first DB from Welcome page', async t => { await t.expect(myRedisDatabasePage.AddRedisDatabase.welcomePageTitle.exists).ok('The welcome page title not displayed'); // Add database from Welcome page - await addNewStandaloneDatabase(ossStandaloneConfig); + await databaseHelper.addNewStandaloneDatabase(ossStandaloneConfig); await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).exists).ok('The database not added', { timeout: 10000 }); }); // update after resolving testcafe Native Automation mode limitations diff --git a/tests/e2e/tests/smoke/database/add-sentinel-db.e2e.ts b/tests/e2e/tests/smoke/database/add-sentinel-db.e2e.ts index 4412ca17b0..36cef9d481 100644 --- a/tests/e2e/tests/smoke/database/add-sentinel-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/add-sentinel-db.e2e.ts @@ -1,15 +1,16 @@ -import { acceptLicenseTerms, discoverSentinelDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossSentinelConfig } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; import { MyRedisDatabasePage } from '../../../pageObjects'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); fixture `Add DBs from Sentinel` .page(commonUrl) .meta({ type: 'smoke'}) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }) .afterEach(async() => { //Delete database @@ -18,6 +19,6 @@ fixture `Add DBs from Sentinel` }); test .meta({ env: env.web, rte: rte.standalone })('Verify that user can add Sentinel DB', async t => { - await discoverSentinelDatabase(ossSentinelConfig); + await databaseHelper.discoverSentinelDatabase(ossSentinelConfig); await t.expect(myRedisDatabasePage.hostPort.textContent).eql(`${ossSentinelConfig.sentinelHost}:${ossSentinelConfig.sentinelPort}`, 'The sentinel database is not in the list'); }); 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 955c93e8a4..e2a4eee969 100644 --- a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts @@ -1,10 +1,6 @@ import { t } from 'testcafe'; import { Chance } from 'chance'; -import { - addOSSClusterDatabase, - acceptLicenseTerms, - deleteDatabase -} from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, ossStandaloneConfig, @@ -18,6 +14,7 @@ const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const telemetry = new Telemetry(); const chance = new Chance(); +const databaseHelper = new DatabaseHelper(); const logger = telemetry.createLogger(); const telemetryEvent = 'CONFIG_DATABASES_OPEN_DATABASE'; @@ -38,13 +35,13 @@ fixture `Add database` .meta({ type: 'smoke' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); test .meta({ rte: rte.standalone }) .requestHooks(logger) .after(async() => { - await deleteDatabase(databaseName); + await databaseHelper.deleteDatabase(databaseName); })('Verify that user can add Standalone Database', async() => { const connectionTimeout = '20'; databaseName = `test_standalone-${chance.string({ length: 10 })}`; @@ -85,9 +82,9 @@ test test .meta({ env: env.web, rte: rte.ossCluster }) .after(async() => { - await deleteDatabase(ossClusterConfig.ossClusterDatabaseName); + await databaseHelper.deleteDatabase(ossClusterConfig.ossClusterDatabaseName); })('Verify that user can add OSS Cluster DB', async() => { - await addOSSClusterDatabase(ossClusterConfig); + await databaseHelper.addOSSClusterDatabase(ossClusterConfig); // Verify new connection badge for OSS cluster await myRedisDatabasePage.verifyDatabaseStatusIsVisible(ossClusterConfig.ossClusterDatabaseName); }); diff --git a/tests/e2e/tests/smoke/database/autodiscover-db.e2e.ts b/tests/e2e/tests/smoke/database/autodiscover-db.e2e.ts index 3b8adc5921..892e4c4077 100644 --- a/tests/e2e/tests/smoke/database/autodiscover-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/autodiscover-db.e2e.ts @@ -1,11 +1,5 @@ import { t } from 'testcafe'; -import { - addNewREClusterDatabase, - acceptLicenseTerms, - deleteDatabase, - addRECloudDatabase, - autodiscoverRECloudDatabase -} from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { commonUrl, redisEnterpriseClusterConfig, @@ -13,9 +7,9 @@ import { } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; import { MyRedisDatabasePage } from '../../../pageObjects'; -import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); let databaseName: string; @@ -23,14 +17,14 @@ fixture `Add database` .meta({ type: 'smoke' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); test .meta({ rte: rte.reCluster }) .after(async() => { - await deleteDatabase(redisEnterpriseClusterConfig.databaseName); + await databaseHelper.deleteDatabase(redisEnterpriseClusterConfig.databaseName); })('Verify that user can add database from RE Cluster via auto-discover flow', async() => { - await addNewREClusterDatabase(redisEnterpriseClusterConfig); + await databaseHelper.addNewREClusterDatabase(redisEnterpriseClusterConfig); // Verify that user can see an indicator of databases that are added using autodiscovery and not opened yet // Verify new connection badge for RE cluster await myRedisDatabasePage.verifyDatabaseStatusIsVisible(redisEnterpriseClusterConfig.databaseName); @@ -38,9 +32,9 @@ test test .meta({ rte: rte.reCloud }) .after(async() => { - await deleteDatabase(cloudDatabaseConfig.databaseName); + await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName); })('Verify that user can add database from RE Cloud', async() => { - await addRECloudDatabase(cloudDatabaseConfig); + await databaseHelper.addRECloudDatabase(cloudDatabaseConfig); // Verify new connection badge for RE cloud await myRedisDatabasePage.verifyDatabaseStatusIsVisible(cloudDatabaseConfig.databaseName); // Verify redis stack icon for RE Cloud with all 5 modules @@ -52,7 +46,7 @@ test // await deleteDatabase(databaseName); })('Verify that user can connect to the RE Cloud database via auto-discover flow', async t => { // Verify that user can see the Cloud auto-discovery option selected by default when switching to the auto-discovery of databases - databaseName = await autodiscoverRECloudDatabase(cloudDatabaseConfig.accessKey, cloudDatabaseConfig.secretKey); + databaseName = await databaseHelper.autodiscoverRECloudDatabase(cloudDatabaseConfig.accessKey, cloudDatabaseConfig.secretKey); // uncomment when fixed db will be added to cloud subscription // await myRedisDatabasePage.clickOnDBByName(databaseName); // // Verify that user can add database from fixed subscription diff --git a/tests/e2e/tests/smoke/database/connecting-to-the-db.e2e.ts b/tests/e2e/tests/smoke/database/connecting-to-the-db.e2e.ts index ff7b21f486..55531b20bf 100644 --- a/tests/e2e/tests/smoke/database/connecting-to-the-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/connecting-to-the-db.e2e.ts @@ -1,5 +1,5 @@ import { ClientFunction } from 'testcafe'; -import { acceptLicenseTerms, deleteDatabase, discoverSentinelDatabase, addOSSClusterDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, @@ -9,6 +9,7 @@ import { import { env, rte } from '../../../helpers/constants'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); const getPageUrl = ClientFunction(() => window.location.href); @@ -16,7 +17,7 @@ fixture `Connecting to the databases verifications` .meta({ type: 'smoke' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); test .meta({ env: env.web, rte: rte.sentinel }) @@ -26,7 +27,7 @@ test await myRedisDatabasePage.deleteDatabaseByName(ossSentinelConfig.masters[1].name); })('Verify that user can connect to Sentinel DB', async t => { // Add OSS Sentinel DB - await discoverSentinelDatabase(ossSentinelConfig); + await databaseHelper.discoverSentinelDatabase(ossSentinelConfig); // Get groups & their count const sentinelGroups = myRedisDatabasePage.dbNameList.withText('primary-group'); @@ -50,10 +51,10 @@ test test .meta({ env: env.web, rte: rte.ossCluster }) .after(async() => { - await deleteDatabase(ossClusterConfig.ossClusterDatabaseName); + await databaseHelper.deleteDatabase(ossClusterConfig.ossClusterDatabaseName); })('Verify that user can connect to OSS Cluster DB', async t => { // Add OSS Cluster DB - await addOSSClusterDatabase(ossClusterConfig); + await databaseHelper.addOSSClusterDatabase(ossClusterConfig); await myRedisDatabasePage.clickOnDBByName(ossClusterConfig.ossClusterDatabaseName); // Check that browser page was opened await t.expect(getPageUrl()).contains('browser', 'Browser page not opened'); diff --git a/tests/e2e/tests/smoke/database/delete-the-db.e2e.ts b/tests/e2e/tests/smoke/database/delete-the-db.e2e.ts index 9f7d735d01..87338bc436 100644 --- a/tests/e2e/tests/smoke/database/delete-the-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/delete-the-db.e2e.ts @@ -1,11 +1,13 @@ import { Chance } from 'chance'; -import { addNewStandaloneDatabase, acceptLicenseTerms } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { rte } from '../../../helpers/constants'; import { MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -const chance = new Chance(); +const chance = new Chance(); const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseHelper = new DatabaseHelper(); + const uniqueId = chance.string({ length: 10 }); let database = { ...ossStandaloneConfig, @@ -16,7 +18,7 @@ fixture `Delete database` .meta({ type: 'smoke' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); database = { ...ossStandaloneConfig, databaseName: `test_standalone-${uniqueId}` @@ -24,7 +26,7 @@ fixture `Delete database` }); test .meta({ rte: rte.standalone })('Verify that user can delete databases', async t => { - await addNewStandaloneDatabase(database); + await databaseHelper.addNewStandaloneDatabase(database); await myRedisDatabasePage.deleteDatabaseByName(database.databaseName); await t.expect(myRedisDatabasePage.dbNameList.withExactText(database.databaseName).exists).notOk('The database not deleted', { timeout: 10000 }); }); diff --git a/tests/e2e/tests/smoke/database/edit-db.e2e.ts b/tests/e2e/tests/smoke/database/edit-db.e2e.ts index 9264bfbd8e..0664a76bb0 100644 --- a/tests/e2e/tests/smoke/database/edit-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/edit-db.e2e.ts @@ -1,5 +1,5 @@ import { ClientFunction } from 'testcafe'; -import { acceptLicenseTerms, deleteDatabase, addNewStandaloneDatabase, addNewREClusterDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, @@ -11,12 +11,13 @@ import { UserAgreementDialog } from '../../../pageObjects/dialogs'; const myRedisDatabasePage = new MyRedisDatabasePage(); const userAgreementDialog = new UserAgreementDialog(); +const databaseHelper = new DatabaseHelper(); fixture `Edit Databases` .meta({ type: 'smoke' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); + await databaseHelper.acceptLicenseTerms(); }); // Returns the URL of the current web page const getPageUrl = ClientFunction(() => window.location.href); @@ -24,9 +25,9 @@ test .meta({ rte: rte.reCluster }) .after(async() => { // Delete database - await deleteDatabase(redisEnterpriseClusterConfig.databaseName); + await databaseHelper.deleteDatabase(redisEnterpriseClusterConfig.databaseName); })('Verify that user can connect to the RE cluster database', async t => { - await addNewREClusterDatabase(redisEnterpriseClusterConfig); + await databaseHelper.addNewREClusterDatabase(redisEnterpriseClusterConfig); await myRedisDatabasePage.clickOnDBByName(redisEnterpriseClusterConfig.databaseName); await t.expect(getPageUrl()).contains('browser', 'The edit view is not opened'); }); @@ -34,11 +35,11 @@ test .meta({ rte: rte.standalone }) .after(async() => { // Delete database - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseHelper.deleteDatabase(ossStandaloneConfig.databaseName); })('Verify that user open edit view of database', async t => { await userAgreementDialog.acceptLicenseTerms(); await t.expect(myRedisDatabasePage.AddRedisDatabase.addDatabaseButton.exists).ok('The add redis database view not found', { timeout: 10000 }); - await addNewStandaloneDatabase(ossStandaloneConfig); + await databaseHelper.addNewStandaloneDatabase(ossStandaloneConfig); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await t.expect(getPageUrl()).contains('browser', 'Browser page not opened'); }); diff --git a/tests/e2e/tests/smoke/workbench/json-workbench.e2e.ts b/tests/e2e/tests/smoke/workbench/json-workbench.e2e.ts index 4310670203..743ccb73c0 100644 --- a/tests/e2e/tests/smoke/workbench/json-workbench.e2e.ts +++ b/tests/e2e/tests/smoke/workbench/json-workbench.e2e.ts @@ -1,11 +1,14 @@ import { rte } from '../../../helpers/constants'; -import { acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase } from '../../../helpers/database'; +import { DatabaseHelper } from '../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); let indexName = Common.generateWord(10); @@ -13,7 +16,7 @@ fixture `JSON verifications at Workbench` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) @@ -21,7 +24,7 @@ fixture `JSON verifications at Workbench` // Clear and delete database await t.switchToMainWindow(); await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteDatabase(ossStandaloneRedisearch.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); test('Verify that user can execute redisearch command for JSON data type in Workbench', async t => { indexName = Common.generateWord(10); diff --git a/tests/e2e/tests/smoke/workbench/scripting-area.e2e.ts b/tests/e2e/tests/smoke/workbench/scripting-area.e2e.ts index 2389d56ef7..35abcb7f9d 100644 --- a/tests/e2e/tests/smoke/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/smoke/workbench/scripting-area.e2e.ts @@ -1,28 +1,25 @@ -import { acceptTermsAddDatabaseOrConnectToRedisStack, deleteDatabase } from '../../../helpers/database'; -import { - MyRedisDatabasePage, - WorkbenchPage -} from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; +import { DatabaseHelper } from '../../../helpers/database'; +import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; +import { DatabaseAPIRequests } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Scripting area at Workbench` .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database - await deleteDatabase(ossStandaloneConfig.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can comment out any characters in scripting area and all these characters in this raw number are not send in the request', async t => { const command1 = 'info'; From cd5fd90ca2d4b2d2fe64f24acab3c1b6369bb44f Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Fri, 7 Jul 2023 16:22:49 +0200 Subject: [PATCH 049/106] fix for ts compilation failure --- tests/e2e/common-actions/databases-actions.ts | 5 +++-- tests/e2e/helpers/api/api-common.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/e2e/common-actions/databases-actions.ts b/tests/e2e/common-actions/databases-actions.ts index 1317a65c3e..21ef295871 100644 --- a/tests/e2e/common-actions/databases-actions.ts +++ b/tests/e2e/common-actions/databases-actions.ts @@ -1,9 +1,10 @@ import { Selector, t } from 'testcafe'; import * as fs from 'fs'; import { MyRedisDatabasePage } from '../pageObjects'; -import { getDatabaseIdByName } from '../helpers/api/api-database'; +import { DatabaseAPIRequests } from '../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const databaseAPIRequests = new DatabaseAPIRequests(); export class DatabasesActions { /** @@ -43,7 +44,7 @@ export class DatabasesActions { */ async selectDatabasesByNames(databases: string[]): Promise { for (const db of databases) { - const databaseId = await getDatabaseIdByName(db); + const databaseId = await databaseAPIRequests.getDatabaseIdByName(db); const databaseCheckbox = Selector(`[data-test-subj=checkboxSelectRow-${databaseId}]`); await t.click(databaseCheckbox); } diff --git a/tests/e2e/helpers/api/api-common.ts b/tests/e2e/helpers/api/api-common.ts index e6aaa55319..6a2959b06e 100644 --- a/tests/e2e/helpers/api/api-common.ts +++ b/tests/e2e/helpers/api/api-common.ts @@ -1,4 +1,4 @@ -import request from 'supertest'; +import * as request from 'supertest'; import { Common } from '../common'; import { Methods } from '../constants'; From 4841c7a422c57b044377df4712387c3af41df66a Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 10 Jul 2023 12:57:12 +0700 Subject: [PATCH 050/106] #RI-4608 - remove GRAPH commands from command helper --- .../CommandHelperWrapper.spec.tsx | 17 +++++++++++ .../command-helper/CommandHelperWrapper.tsx | 15 ++++++++-- redisinsight/ui/src/constants/keys.ts | 1 - .../ui/src/constants/workbenchResults.ts | 9 ------ .../ui/src/slices/interfaces/instances.ts | 1 - redisinsight/ui/src/utils/cliHelper.tsx | 15 ++++++++-- .../ui/src/utils/tests/cliHelper.spec.ts | 28 ++++++++++++++++++- 7 files changed, 68 insertions(+), 18 deletions(-) diff --git a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.spec.tsx b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.spec.tsx index ac6945b0c6..77d43b872a 100644 --- a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.spec.tsx +++ b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.spec.tsx @@ -193,4 +193,21 @@ describe('CliBodyWrapper', () => { unmount() }) }) + + it('should not since when matched command is deprecated', () => { + const sinceId = 'cli-helper-since' + const cliHelperDefaultId = 'cli-helper-default' + + cliSettingsSelector.mockImplementation(() => ({ + matchedCommand: 'GRAPH.CONFIG SET', + isEnteringCommand: true, + })) + + const { unmount, queryByTestId } = render() + + expect(queryByTestId(cliHelperDefaultId)).toBeInTheDocument() + expect(queryByTestId(sinceId)).not.toBeInTheDocument() + + unmount() + }) }) diff --git a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx index 4c87c4f259..14fe7d4332 100644 --- a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx +++ b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx @@ -11,7 +11,13 @@ import { import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' -import { generateArgs, generateArgsNames, getComplexityShortNotation } from 'uiSrc/utils' +import { + generateArgs, + generateArgsNames, + getComplexityShortNotation, + removeDeprecatedModuleCommands, + checkDeprecatedModuleCommand, +} from 'uiSrc/utils' import CommandHelper from './CommandHelper' import CommandHelperHeader from './CommandHelperHeader' @@ -26,9 +32,12 @@ const CommandHelperWrapper = () => { searchingCommand, searchingCommandFilter } = useSelector(cliSettingsSelector) - const { spec: ALL_REDIS_COMMANDS, commandsArray: KEYS_OF_COMMANDS } = useSelector(appRedisCommandsSelector) + const { spec: ALL_REDIS_COMMANDS, commandsArray } = useSelector(appRedisCommandsSelector) const { instanceId = '' } = useParams<{ instanceId: string }>() - const lastMatchedCommand = (isEnteringCommand && matchedCommand) ? matchedCommand : searchedCommand + const lastMatchedCommand = (isEnteringCommand && matchedCommand && !checkDeprecatedModuleCommand(matchedCommand)) + ? matchedCommand + : searchedCommand + const KEYS_OF_COMMANDS = removeDeprecatedModuleCommands(commandsArray) let searchedCommands: string[] = [] useEffect(() => { diff --git a/redisinsight/ui/src/constants/keys.ts b/redisinsight/ui/src/constants/keys.ts index 2cce1b921d..94310dd617 100644 --- a/redisinsight/ui/src/constants/keys.ts +++ b/redisinsight/ui/src/constants/keys.ts @@ -27,7 +27,6 @@ export const GROUP_TYPES_DISPLAY = Object.freeze({ [KeyTypes.ReJSON]: 'JSON', [KeyTypes.JSON]: 'JSON', [KeyTypes.Stream]: 'Stream', - [ModulesKeyTypes.Graph]: 'Graph', [ModulesKeyTypes.TimeSeries]: 'TS', [CommandGroup.Bitmap]: 'Bitmap', [CommandGroup.Cluster]: 'Cluster', diff --git a/redisinsight/ui/src/constants/workbenchResults.ts b/redisinsight/ui/src/constants/workbenchResults.ts index 75aac0ea1b..8a1314e799 100644 --- a/redisinsight/ui/src/constants/workbenchResults.ts +++ b/redisinsight/ui/src/constants/workbenchResults.ts @@ -24,14 +24,6 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = additionalText: ['These features enable multi-field queries, aggregation, exact phrase matching, numeric filtering, ', 'geo filtering and vector similarity semantic search on top of text queries.'], link: 'https://redis.io/docs/stack/search/' }, - [RedisDefaultModules.Graph]: { - text: ['RedisGraph adds a Property Graph data structure to Redis. ', 'With this capability you can:'], - improvements: [ - 'Create graphs', - 'Query property graphs using the Cypher query language with proprietary extensions' - ], - link: 'https://redis.io/docs/stack/graph/' - }, [RedisDefaultModules.ReJSON]: { text: ['RedisJSON adds the capability to:'], improvements: [ @@ -58,7 +50,6 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = export const MODULE_TEXT_VIEW: { [key in RedisDefaultModules]?: string } = { [RedisDefaultModules.Bloom]: 'RedisBloom', - [RedisDefaultModules.Graph]: 'RedisGraph', [RedisDefaultModules.ReJSON]: 'RedisJSON', [RedisDefaultModules.Search]: 'RediSearch', [RedisDefaultModules.TimeSeries]: 'RedisTimeSeries', diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index a296001fc0..7c2256a166 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -183,7 +183,6 @@ export const COMMAND_MODULES = { [RedisDefaultModules.Search]: REDISEARCH_MODULES, [RedisDefaultModules.ReJSON]: [RedisDefaultModules.ReJSON], [RedisDefaultModules.TimeSeries]: [RedisDefaultModules.TimeSeries], - [RedisDefaultModules.Graph]: [RedisDefaultModules.Graph], [RedisDefaultModules.Bloom]: [RedisDefaultModules.Bloom], } diff --git a/redisinsight/ui/src/utils/cliHelper.tsx b/redisinsight/ui/src/utils/cliHelper.tsx index 6a6df5b20f..61888a1631 100644 --- a/redisinsight/ui/src/utils/cliHelper.tsx +++ b/redisinsight/ui/src/utils/cliHelper.tsx @@ -162,9 +162,6 @@ const checkCommandModule = (command: string) => { case command.startsWith(ModuleCommandPrefix.TimeSeries): { return RedisDefaultModules.TimeSeries } - case command.startsWith(ModuleCommandPrefix.Graph): { - return RedisDefaultModules.Graph - } case command.startsWith(ModuleCommandPrefix.BF): case command.startsWith(ModuleCommandPrefix.CF): case command.startsWith(ModuleCommandPrefix.CMS): @@ -221,6 +218,16 @@ const getCommandNameFromQuery = ( } } +const DEPRECATED_MODULES_PREFIXES = [ + ModuleCommandPrefix.Graph +] + +const checkDeprecatedModuleCommand = (command: string) => + DEPRECATED_MODULES_PREFIXES.some((prefix) => command.startsWith(prefix)) + +const removeDeprecatedModuleCommands = (commands: string[]) => commands + .filter((command) => !checkDeprecatedModuleCommand(command)) + export { cliParseTextResponse, cliParseTextResponseWithOffset, @@ -239,4 +246,6 @@ export { getCommandNameFromQuery, wbSummaryCommand, replaceEmptyValue, + removeDeprecatedModuleCommands, + checkDeprecatedModuleCommand, } diff --git a/redisinsight/ui/src/utils/tests/cliHelper.spec.ts b/redisinsight/ui/src/utils/tests/cliHelper.spec.ts index 00806f402d..9fe0c0aea6 100644 --- a/redisinsight/ui/src/utils/tests/cliHelper.spec.ts +++ b/redisinsight/ui/src/utils/tests/cliHelper.spec.ts @@ -9,6 +9,8 @@ import { checkUnsupportedCommand, checkBlockingCommand, replaceEmptyValue, + removeDeprecatedModuleCommands, + checkDeprecatedModuleCommand, } from 'uiSrc/utils' import { MOCK_COMMANDS_SPEC } from 'uiSrc/constants' import { render, screen } from 'uiSrc/utils/test-utils' @@ -89,7 +91,6 @@ const checkCommandModuleTests = [ { input: 'FT.foo bar', expected: RedisDefaultModules.Search }, { input: 'JSON.foo bar', expected: RedisDefaultModules.ReJSON }, { input: 'TS.foo bar', expected: RedisDefaultModules.TimeSeries }, - { input: 'GRAPH.foo bar', expected: RedisDefaultModules.Graph }, { input: 'BF.foo bar', expected: RedisDefaultModules.Bloom }, { input: 'CF.foo bar', expected: RedisDefaultModules.Bloom }, { input: 'CMS.foo bar', expected: RedisDefaultModules.Bloom }, @@ -110,6 +111,19 @@ const checkBlockingCommandTests = [ { input: [['foo', 'bar'], 'FT.foo bar'], expected: undefined }, ] +const checkDeprecatedModuleCommandTests = [ + { input: 'FT.foo bar', expected: false }, + { input: 'GRAPH foo bar', expected: false }, + { input: 'GRAPH.foo bar', expected: true }, + { input: 'FOO bar', expected: false }, +] + +const removeDeprecatedModuleCommandsTests = [ + { input: ['FT.foo'], expected: ['FT.foo'] }, + { input: ['GRAPH.foo', 'FT.foo'], expected: ['FT.foo'] }, + { input: ['FOO', 'GRAPH.FOO', 'CF.FOO', 'GRAPH.BAR'], expected: ['FOO', 'CF.FOO'] }, +] + describe('getCommandNameFromQuery', () => { test.each(getCommandNameFromQueryTests)('%j', ({ input, expected }) => { // @ts-ignore @@ -185,3 +199,15 @@ describe('checkBlockingCommand', () => { expect(checkBlockingCommand(...input)).toEqual(expected) }) }) + +describe('checkDeprecatedModuleCommand', () => { + test.each(checkDeprecatedModuleCommandTests)('%j', ({ input, expected }) => { + expect(checkDeprecatedModuleCommand(input)).toEqual(expected) + }) +}) + +describe('removeDeprecatedModuleCommands', () => { + test.each(removeDeprecatedModuleCommandsTests)('%j', ({ input, expected }) => { + expect(removeDeprecatedModuleCommands(input)).toEqual(expected) + }) +}) From 8aa08a49ebb2706bf1a43e9f4f6d29c676091e7d Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 10 Jul 2023 13:01:14 +0700 Subject: [PATCH 051/106] update text --- .../src/components/command-helper/CommandHelperWrapper.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.spec.tsx b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.spec.tsx index 77d43b872a..e92246b6fb 100644 --- a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.spec.tsx +++ b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.spec.tsx @@ -194,7 +194,7 @@ describe('CliBodyWrapper', () => { }) }) - it('should not since when matched command is deprecated', () => { + it('should render default message when matched command is deprecated', () => { const sinceId = 'cli-helper-since' const cliHelperDefaultId = 'cli-helper-default' From 234f2971ff277442e2d7d10192bc80a1e1536e6e Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 10 Jul 2023 14:08:42 +0700 Subject: [PATCH 052/106] #RI-4586 - add redisgears 2 icon and text --- .../database-list-modules/DatabaseListModules.tsx | 10 ++++++++++ redisinsight/ui/src/slices/interfaces/instances.ts | 9 ++++++++- redisinsight/ui/src/telemetry/interfaces.ts | 1 + .../ui/src/telemetry/telemetryUtils.spec.ts | 3 +++ redisinsight/ui/src/telemetry/telemetryUtils.ts | 13 ++++++++++++- redisinsight/ui/src/utils/modules.ts | 6 +++++- 6 files changed, 39 insertions(+), 3 deletions(-) diff --git a/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx b/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx index 299b6c5959..191eb35fe1 100644 --- a/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx +++ b/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx @@ -69,6 +69,16 @@ export const modulesDefaultInit = { iconLight: RedisGraphLight, text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.Graph], }, + [RedisDefaultModules.RedisGears]: { + iconDark: RedisGearsDark, + iconLight: RedisGearsLight, + text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.RedisGears], + }, + [RedisDefaultModules.RedisGears2]: { + iconDark: RedisGearsDark, + iconLight: RedisGearsLight, + text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.RedisGears2], + }, [RedisDefaultModules.ReJSON]: { iconDark: RedisJSONDark, iconLight: RedisJSONLight, diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index a296001fc0..60fb0c70fe 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -179,6 +179,11 @@ export const REDISEARCH_MODULES: string[] = [ RedisDefaultModules.FTL, ] +export const TRIGGERED_AND_FUNCTIONS_MODULES: string[] = [ + RedisDefaultModules.RedisGears, + RedisDefaultModules.RedisGears2, +] + export const COMMAND_MODULES = { [RedisDefaultModules.Search]: REDISEARCH_MODULES, [RedisDefaultModules.ReJSON]: [RedisDefaultModules.ReJSON], @@ -188,6 +193,7 @@ export const COMMAND_MODULES = { } const RediSearchModulesText = [...REDISEARCH_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'RediSearch' }), {}) +const TriggeredAndFunctionsModulesText = [...TRIGGERED_AND_FUNCTIONS_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'Triggered & Functions' }), {}) // Enums don't allow to use dynamic key export const DATABASE_LIST_MODULES_TEXT = Object.freeze({ @@ -199,7 +205,8 @@ export const DATABASE_LIST_MODULES_TEXT = Object.freeze({ [RedisDefaultModules.TimeSeries]: 'RedisTimeSeries', [RedisCustomModulesName.Proto]: 'redis-protobuf', [RedisCustomModulesName.IpTables]: 'RedisPushIpTables', - ...RediSearchModulesText + ...RediSearchModulesText, + ...TriggeredAndFunctionsModulesText, }) export enum AddRedisClusterDatabaseOptions { diff --git a/redisinsight/ui/src/telemetry/interfaces.ts b/redisinsight/ui/src/telemetry/interfaces.ts index ee9243509b..ef0a384a67 100644 --- a/redisinsight/ui/src/telemetry/interfaces.ts +++ b/redisinsight/ui/src/telemetry/interfaces.ts @@ -52,6 +52,7 @@ export enum RedisModules { RedisJSON = 'ReJSON', RediSearch = 'search', RedisTimeSeries = 'timeseries', + 'Triggered & Functions' = 'redisgears' } interface IModuleSummary { diff --git a/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts b/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts index be6164986f..b6e433a3d7 100644 --- a/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts +++ b/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts @@ -9,6 +9,7 @@ const DEFAULT_SUMMARY = Object.freeze( RedisBloom: { loaded: false }, RedisJSON: { loaded: false }, RedisTimeSeries: { loaded: false }, + 'Triggered & Functions': { loaded: false }, customModules: [], }, ) @@ -43,6 +44,7 @@ const getRedisModulesSummaryTests = [ { name: 'ReJSON', version: 10000, semanticVersion: '1.0.0' }, { name: 'search', version: 10000, semanticVersion: '1.0.0' }, { name: 'timeseries', version: 10000, semanticVersion: '1.0.0' }, + { name: 'redisgears_2', version: 10000, semanticVersion: '1.0.0' }, ], expected: { RedisAI: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, @@ -52,6 +54,7 @@ const getRedisModulesSummaryTests = [ RedisJSON: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RediSearch: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RedisTimeSeries: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, + 'Triggered & Functions': { loaded: true, version: 10000, semanticVersion: '1.0.0' }, customModules: [], }, }, diff --git a/redisinsight/ui/src/telemetry/telemetryUtils.ts b/redisinsight/ui/src/telemetry/telemetryUtils.ts index ed4659cc55..d9d3529275 100644 --- a/redisinsight/ui/src/telemetry/telemetryUtils.ts +++ b/redisinsight/ui/src/telemetry/telemetryUtils.ts @@ -5,7 +5,7 @@ import isGlob from 'is-glob' import { cloneDeep } from 'lodash' import * as jsonpath from 'jsonpath' -import { isRedisearchAvailable, Nullable } from 'uiSrc/utils' +import { isRedisearchAvailable, isTriggeredAndFunctionsAvailable, Nullable } from 'uiSrc/utils' import { localStorageService } from 'uiSrc/services' import { ApiEndpoints, BrowserStorageItem, KeyTypes, StreamViews } from 'uiSrc/constants' import { KeyViewType } from 'uiSrc/slices/interfaces/keys' @@ -219,6 +219,7 @@ const DEFAULT_SUMMARY: IRedisModulesSummary = Object.freeze( RedisBloom: { loaded: false }, RedisJSON: { loaded: false }, RedisTimeSeries: { loaded: false }, + 'Triggered & Functions': { loaded: false }, customModules: [], }, ) @@ -253,6 +254,16 @@ const getRedisModulesSummary = (modules: AdditionalRedisModule[] = []): IRedisMo return } + if (isTriggeredAndFunctionsAvailable([module])) { + const triggeredAndFunctionsName = getEnumKeyBValue(RedisModules, RedisModules['Triggered & Functions']) + summary[triggeredAndFunctionsName] = { + loaded: true, + version: module.version, + semanticVersion: module.semanticVersion, + } + return + } + summary.customModules.push(module) })) } catch (e) { diff --git a/redisinsight/ui/src/utils/modules.ts b/redisinsight/ui/src/utils/modules.ts index b5cab6cdad..edddadc2e3 100644 --- a/redisinsight/ui/src/utils/modules.ts +++ b/redisinsight/ui/src/utils/modules.ts @@ -1,4 +1,4 @@ -import { DATABASE_LIST_MODULES_TEXT, RedisDefaultModules, REDISEARCH_MODULES } from 'uiSrc/slices/interfaces' +import { DATABASE_LIST_MODULES_TEXT, RedisDefaultModules, REDISEARCH_MODULES, TRIGGERED_AND_FUNCTIONS_MODULES } from 'uiSrc/slices/interfaces' import { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module' export interface IDatabaseModule { @@ -41,5 +41,9 @@ export const isRedisearchAvailable = (modules: AdditionalRedisModule[]): boolean modules?.some(({ name }) => REDISEARCH_MODULES.some((search) => name === search)) +export const isTriggeredAndFunctionsAvailable = (modules: AdditionalRedisModule[]): boolean => + modules?.some(({ name }) => + TRIGGERED_AND_FUNCTIONS_MODULES.some((value) => name === value)) + export const isContainJSONModule = (modules: AdditionalRedisModule[]): boolean => modules?.some((m: AdditionalRedisModule) => m.name === RedisDefaultModules.ReJSON) From 3a3f76fc78ed0edb8e43d2e51919ebf4b9bde5b2 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 10 Jul 2023 15:53:20 +0700 Subject: [PATCH 053/106] #RI-4586 - resolve comments --- redisinsight/ui/src/slices/interfaces/instances.ts | 2 +- redisinsight/ui/src/telemetry/interfaces.ts | 4 ++-- redisinsight/ui/src/telemetry/telemetryUtils.spec.ts | 4 ++-- redisinsight/ui/src/telemetry/telemetryUtils.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index 60fb0c70fe..8e8403cb50 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -193,7 +193,7 @@ export const COMMAND_MODULES = { } const RediSearchModulesText = [...REDISEARCH_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'RediSearch' }), {}) -const TriggeredAndFunctionsModulesText = [...TRIGGERED_AND_FUNCTIONS_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'Triggered & Functions' }), {}) +const TriggeredAndFunctionsModulesText = [...TRIGGERED_AND_FUNCTIONS_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'Triggers & Functions' }), {}) // Enums don't allow to use dynamic key export const DATABASE_LIST_MODULES_TEXT = Object.freeze({ diff --git a/redisinsight/ui/src/telemetry/interfaces.ts b/redisinsight/ui/src/telemetry/interfaces.ts index ef0a384a67..8930aa1fd2 100644 --- a/redisinsight/ui/src/telemetry/interfaces.ts +++ b/redisinsight/ui/src/telemetry/interfaces.ts @@ -52,13 +52,13 @@ export enum RedisModules { RedisJSON = 'ReJSON', RediSearch = 'search', RedisTimeSeries = 'timeseries', - 'Triggered & Functions' = 'redisgears' + 'Triggers & Functions' = 'redisgears' } interface IModuleSummary { loaded: boolean version?: number - semanticVersion?: number + semanticVersion?: string } export interface IRedisModulesSummary extends Record { customModules: AdditionalRedisModule[] diff --git a/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts b/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts index b6e433a3d7..95cd572f8a 100644 --- a/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts +++ b/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts @@ -9,7 +9,7 @@ const DEFAULT_SUMMARY = Object.freeze( RedisBloom: { loaded: false }, RedisJSON: { loaded: false }, RedisTimeSeries: { loaded: false }, - 'Triggered & Functions': { loaded: false }, + 'Triggers & Functions': { loaded: false }, customModules: [], }, ) @@ -54,7 +54,7 @@ const getRedisModulesSummaryTests = [ RedisJSON: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RediSearch: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RedisTimeSeries: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, - 'Triggered & Functions': { loaded: true, version: 10000, semanticVersion: '1.0.0' }, + 'Triggers & Functions': { loaded: true, version: 10000, semanticVersion: '1.0.0' }, customModules: [], }, }, diff --git a/redisinsight/ui/src/telemetry/telemetryUtils.ts b/redisinsight/ui/src/telemetry/telemetryUtils.ts index d9d3529275..f1b90b77d4 100644 --- a/redisinsight/ui/src/telemetry/telemetryUtils.ts +++ b/redisinsight/ui/src/telemetry/telemetryUtils.ts @@ -219,7 +219,7 @@ const DEFAULT_SUMMARY: IRedisModulesSummary = Object.freeze( RedisBloom: { loaded: false }, RedisJSON: { loaded: false }, RedisTimeSeries: { loaded: false }, - 'Triggered & Functions': { loaded: false }, + 'Triggers & Functions': { loaded: false }, customModules: [], }, ) @@ -255,7 +255,7 @@ const getRedisModulesSummary = (modules: AdditionalRedisModule[] = []): IRedisMo } if (isTriggeredAndFunctionsAvailable([module])) { - const triggeredAndFunctionsName = getEnumKeyBValue(RedisModules, RedisModules['Triggered & Functions']) + const triggeredAndFunctionsName = getEnumKeyBValue(RedisModules, RedisModules['Triggers & Functions']) summary[triggeredAndFunctionsName] = { loaded: true, version: module.version, From b9f78c024914d8f014d10ca28fde57e43c729f6f Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 10 Jul 2023 16:52:25 +0700 Subject: [PATCH 054/106] #RI-4586 - resolve comments --- redisinsight/ui/src/telemetry/interfaces.ts | 2 +- .../ui/src/telemetry/telemetryUtils.ts | 25 ++++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/redisinsight/ui/src/telemetry/interfaces.ts b/redisinsight/ui/src/telemetry/interfaces.ts index 8930aa1fd2..4937fd2c7c 100644 --- a/redisinsight/ui/src/telemetry/interfaces.ts +++ b/redisinsight/ui/src/telemetry/interfaces.ts @@ -55,7 +55,7 @@ export enum RedisModules { 'Triggers & Functions' = 'redisgears' } -interface IModuleSummary { +export interface IModuleSummary { loaded: boolean version?: number semanticVersion?: string diff --git a/redisinsight/ui/src/telemetry/telemetryUtils.ts b/redisinsight/ui/src/telemetry/telemetryUtils.ts index f1b90b77d4..2e5bff8fc2 100644 --- a/redisinsight/ui/src/telemetry/telemetryUtils.ts +++ b/redisinsight/ui/src/telemetry/telemetryUtils.ts @@ -11,6 +11,7 @@ import { ApiEndpoints, BrowserStorageItem, KeyTypes, StreamViews } from 'uiSrc/c import { KeyViewType } from 'uiSrc/slices/interfaces/keys' import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import { checkIsAnalyticsGranted, getInfoServer } from 'uiSrc/telemetry/checkAnalytics' +import { IModuleSummary } from 'uiSrc/telemetry/interfaces' import { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module' import { ITelemetrySendEvent, @@ -230,37 +231,31 @@ const getEnumKeyBValue = (myEnum: any, enumValue: number | string): string => { return index > -1 ? keys[index] : '' } +const getModuleSummaryToSent = (module: AdditionalRedisModule): IModuleSummary => ({ + loaded: true, + version: module.version, + semanticVersion: module.semanticVersion, +}) + const getRedisModulesSummary = (modules: AdditionalRedisModule[] = []): IRedisModulesSummary => { const summary = cloneDeep(DEFAULT_SUMMARY) try { modules.forEach(((module) => { if (SUPPORTED_REDIS_MODULES[module.name]) { const moduleName = getEnumKeyBValue(RedisModules, module.name) - summary[moduleName] = { - loaded: true, - version: module.version, - semanticVersion: module.semanticVersion, - } + summary[moduleName as keyof typeof RedisModules] = getModuleSummaryToSent(module) return } if (isRedisearchAvailable([module])) { const redisearchName = getEnumKeyBValue(RedisModules, RedisModules.RediSearch) - summary[redisearchName] = { - loaded: true, - version: module.version, - semanticVersion: module.semanticVersion, - } + summary[redisearchName as keyof typeof RedisModules] = getModuleSummaryToSent(module) return } if (isTriggeredAndFunctionsAvailable([module])) { const triggeredAndFunctionsName = getEnumKeyBValue(RedisModules, RedisModules['Triggers & Functions']) - summary[triggeredAndFunctionsName] = { - loaded: true, - version: module.version, - semanticVersion: module.semanticVersion, - } + summary[triggeredAndFunctionsName as keyof typeof RedisModules] = getModuleSummaryToSent(module) return } From 4a42e4261fbe9ac2e93e762564d553b477ab0ea6 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 10 Jul 2023 17:00:13 +0700 Subject: [PATCH 055/106] #RI-4586 - resolve comments --- redisinsight/api/src/utils/redis-modules-summary.spec.ts | 2 ++ redisinsight/ui/src/telemetry/interfaces.ts | 2 ++ redisinsight/ui/src/telemetry/telemetryUtils.ts | 8 ++++---- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/redisinsight/api/src/utils/redis-modules-summary.spec.ts b/redisinsight/api/src/utils/redis-modules-summary.spec.ts index f80eb973e0..fad97545a0 100644 --- a/redisinsight/api/src/utils/redis-modules-summary.spec.ts +++ b/redisinsight/api/src/utils/redis-modules-summary.spec.ts @@ -43,6 +43,7 @@ const getRedisModulesSummaryTests = [ { name: 'ReJSON', version: 10000, semanticVersion: '1.0.0' }, { name: 'search', version: 10000, semanticVersion: '1.0.0' }, { name: 'timeseries', version: 10000, semanticVersion: '1.0.0' }, + { name: 'redisgears_2', version: 10000, semanticVersion: '1.0.0' }, ], expected: { RedisAI: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, @@ -52,6 +53,7 @@ const getRedisModulesSummaryTests = [ RedisJSON: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RediSearch: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RedisTimeSeries: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, + 'Triggers & Functios': { loaded: true, version: 10000, semanticVersion: '1.0.0' }, customModules: [], }, }, diff --git a/redisinsight/ui/src/telemetry/interfaces.ts b/redisinsight/ui/src/telemetry/interfaces.ts index 4937fd2c7c..a635183707 100644 --- a/redisinsight/ui/src/telemetry/interfaces.ts +++ b/redisinsight/ui/src/telemetry/interfaces.ts @@ -60,6 +60,8 @@ export interface IModuleSummary { version?: number semanticVersion?: string } + +export type RedisModulesKeyType = keyof typeof RedisModules export interface IRedisModulesSummary extends Record { customModules: AdditionalRedisModule[] } diff --git a/redisinsight/ui/src/telemetry/telemetryUtils.ts b/redisinsight/ui/src/telemetry/telemetryUtils.ts index 2e5bff8fc2..99299f7e5b 100644 --- a/redisinsight/ui/src/telemetry/telemetryUtils.ts +++ b/redisinsight/ui/src/telemetry/telemetryUtils.ts @@ -11,7 +11,7 @@ import { ApiEndpoints, BrowserStorageItem, KeyTypes, StreamViews } from 'uiSrc/c import { KeyViewType } from 'uiSrc/slices/interfaces/keys' import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import { checkIsAnalyticsGranted, getInfoServer } from 'uiSrc/telemetry/checkAnalytics' -import { IModuleSummary } from 'uiSrc/telemetry/interfaces' +import { IModuleSummary, RedisModulesKeyType } from 'uiSrc/telemetry/interfaces' import { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module' import { ITelemetrySendEvent, @@ -243,19 +243,19 @@ const getRedisModulesSummary = (modules: AdditionalRedisModule[] = []): IRedisMo modules.forEach(((module) => { if (SUPPORTED_REDIS_MODULES[module.name]) { const moduleName = getEnumKeyBValue(RedisModules, module.name) - summary[moduleName as keyof typeof RedisModules] = getModuleSummaryToSent(module) + summary[moduleName as RedisModulesKeyType] = getModuleSummaryToSent(module) return } if (isRedisearchAvailable([module])) { const redisearchName = getEnumKeyBValue(RedisModules, RedisModules.RediSearch) - summary[redisearchName as keyof typeof RedisModules] = getModuleSummaryToSent(module) + summary[redisearchName as RedisModulesKeyType] = getModuleSummaryToSent(module) return } if (isTriggeredAndFunctionsAvailable([module])) { const triggeredAndFunctionsName = getEnumKeyBValue(RedisModules, RedisModules['Triggers & Functions']) - summary[triggeredAndFunctionsName as keyof typeof RedisModules] = getModuleSummaryToSent(module) + summary[triggeredAndFunctionsName as RedisModulesKeyType] = getModuleSummaryToSent(module) return } From b830c6c01a73b689c28f330bbd0e16d6c7241bd2 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 10 Jul 2023 17:27:36 +0700 Subject: [PATCH 056/106] #RI-4608 - remove deprecated command groups --- .../ui/src/slices/app/redis-commands.ts | 3 +- redisinsight/ui/src/utils/cliHelper.tsx | 14 ++++++-- .../ui/src/utils/tests/cliHelper.spec.ts | 36 +++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/redisinsight/ui/src/slices/app/redis-commands.ts b/redisinsight/ui/src/slices/app/redis-commands.ts index dd0245ec2b..4d28a7de73 100644 --- a/redisinsight/ui/src/slices/app/redis-commands.ts +++ b/redisinsight/ui/src/slices/app/redis-commands.ts @@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit' import { isString, uniqBy } from 'lodash' import { apiService } from 'uiSrc/services' import { ApiEndpoints, ICommand, ICommands } from 'uiSrc/constants' -import { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils' +import { getApiErrorMessage, isStatusSuccessful, checkDeprecatedCommandGroup } from 'uiSrc/utils' import { GetServerInfoResponse } from 'apiSrc/modules/server/dto/server.dto' import { AppDispatch, RootState } from '../store' @@ -31,6 +31,7 @@ const appRedisCommandsSlice = createSlice({ state.commandGroups = uniqBy(Object.values(payload), 'group') .map((item: ICommand) => item.group) .filter((group: string) => isString(group)) + .filter((group: string) => !checkDeprecatedCommandGroup(group)) }, getRedisCommandsFailure: (state, { payload }) => { state.loading = false diff --git a/redisinsight/ui/src/utils/cliHelper.tsx b/redisinsight/ui/src/utils/cliHelper.tsx index 61888a1631..43927d78d5 100644 --- a/redisinsight/ui/src/utils/cliHelper.tsx +++ b/redisinsight/ui/src/utils/cliHelper.tsx @@ -6,7 +6,7 @@ import { isUndefined } from 'lodash' import { localStorageService } from 'uiSrc/services' import { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli' import { resetOutput, updateCliCommandHistory } from 'uiSrc/slices/cli/cli-output' -import { BrowserStorageItem, ICommands } from 'uiSrc/constants' +import { BrowserStorageItem, ICommands, CommandGroup } from 'uiSrc/constants' import { ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants' import { SelectCommand } from 'uiSrc/constants/cliOutput' import { @@ -218,12 +218,19 @@ const getCommandNameFromQuery = ( } } -const DEPRECATED_MODULES_PREFIXES = [ +const DEPRECATED_MODULE_PREFIXES = [ ModuleCommandPrefix.Graph ] +const DEPRECATED_MODULE_GROUPS = [ + CommandGroup.Graph +] + const checkDeprecatedModuleCommand = (command: string) => - DEPRECATED_MODULES_PREFIXES.some((prefix) => command.startsWith(prefix)) + DEPRECATED_MODULE_PREFIXES.some((prefix) => command.startsWith(prefix)) + +const checkDeprecatedCommandGroup = (item: string) => + DEPRECATED_MODULE_GROUPS.some((group) => group === item) const removeDeprecatedModuleCommands = (commands: string[]) => commands .filter((command) => !checkDeprecatedModuleCommand(command)) @@ -248,4 +255,5 @@ export { replaceEmptyValue, removeDeprecatedModuleCommands, checkDeprecatedModuleCommand, + checkDeprecatedCommandGroup, } diff --git a/redisinsight/ui/src/utils/tests/cliHelper.spec.ts b/redisinsight/ui/src/utils/tests/cliHelper.spec.ts index 9fe0c0aea6..2ad752bd99 100644 --- a/redisinsight/ui/src/utils/tests/cliHelper.spec.ts +++ b/redisinsight/ui/src/utils/tests/cliHelper.spec.ts @@ -11,6 +11,7 @@ import { replaceEmptyValue, removeDeprecatedModuleCommands, checkDeprecatedModuleCommand, + checkDeprecatedCommandGroup, } from 'uiSrc/utils' import { MOCK_COMMANDS_SPEC } from 'uiSrc/constants' import { render, screen } from 'uiSrc/utils/test-utils' @@ -124,6 +125,35 @@ const removeDeprecatedModuleCommandsTests = [ { input: ['FOO', 'GRAPH.FOO', 'CF.FOO', 'GRAPH.BAR'], expected: ['FOO', 'CF.FOO'] }, ] +const checkDeprecatedCommandGroupTests = [ + { input: 'cluster', expected: false }, + { input: 'connection', expected: false }, + { input: 'geo', expected: false }, + { input: 'bitmap', expected: false }, + { input: 'generic', expected: false }, + { input: 'pubsub', expected: false }, + { input: 'scripting', expected: false }, + { input: 'transactions', expected: false }, + { input: 'server', expected: false }, + { input: 'sorted-set', expected: false }, + { input: 'hyperloglog', expected: false }, + { input: 'hash', expected: false }, + { input: 'set', expected: false }, + { input: 'stream', expected: false }, + { input: 'list', expected: false }, + { input: 'string', expected: false }, + { input: 'search', expected: false }, + { input: 'json', expected: false }, + { input: 'timeseries', expected: false }, + { input: 'graph', expected: true }, + { input: 'ai', expected: false }, + { input: 'tdigest', expected: false }, + { input: 'cms', expected: false }, + { input: 'topk', expected: false }, + { input: 'bf', expected: false }, + { input: 'cf', expected: false }, +] + describe('getCommandNameFromQuery', () => { test.each(getCommandNameFromQueryTests)('%j', ({ input, expected }) => { // @ts-ignore @@ -211,3 +241,9 @@ describe('removeDeprecatedModuleCommands', () => { expect(removeDeprecatedModuleCommands(input)).toEqual(expected) }) }) + +describe('checkDeprecatedCommandGroup', () => { + test.each(checkDeprecatedCommandGroupTests)('%j', ({ input, expected }) => { + expect(checkDeprecatedCommandGroup(input)).toEqual(expected) + }) +}) From 723c89e53cca41c3a0b2c65b03f8bd3715e166af Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 10 Jul 2023 17:36:24 +0700 Subject: [PATCH 057/106] #RI-4608 - remove deprecated command groups --- .../ui/src/components/command-helper/CommandHelperWrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx index 14fe7d4332..901abf57e9 100644 --- a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx +++ b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx @@ -1,5 +1,5 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui' -import React, { ReactElement, useEffect } from 'react' +import React, { ReactElement, useEffect, useMemo } from 'react' import { useSelector } from 'react-redux' import { useParams } from 'react-router-dom' @@ -37,7 +37,7 @@ const CommandHelperWrapper = () => { const lastMatchedCommand = (isEnteringCommand && matchedCommand && !checkDeprecatedModuleCommand(matchedCommand)) ? matchedCommand : searchedCommand - const KEYS_OF_COMMANDS = removeDeprecatedModuleCommands(commandsArray) + const KEYS_OF_COMMANDS = useMemo(() => removeDeprecatedModuleCommands(commandsArray), [commandsArray]) let searchedCommands: string[] = [] useEffect(() => { From 4e7d3a58483715e2ffba5205f55ad33b8ffa71d0 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Mon, 10 Jul 2023 13:15:22 +0200 Subject: [PATCH 058/106] add test for Open a Stream function --- .../triggers-and-functions-functions-page.ts | 4 +++ .../triggers-and-functions/libraries.e2e.ts | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts b/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts index ba283d57b3..f75884595b 100644 --- a/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts +++ b/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts @@ -11,6 +11,10 @@ export class TriggersAndFunctionsFunctionsPage extends InstancePage { addArgumentItemButton = Selector('[data-testid=add-new-argument-item]'); addKeyNameItemButton = Selector('[data-testid=add-new-key-item]'); runInCliButton = Selector('[data-testid=invoke-function-btn]'); + findKeyButton = Selector('[data-testid=find-key-btn]'); + + // inputs + keyNameStreamFunctions = Selector('[data-testid=keyName-field]'); //Masks // insert name diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index 78cafd3d6e..3336a856ad 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -16,6 +16,7 @@ const triggersAndFunctionsLibrariesPage = new TriggersAndFunctionsLibrariesPage( const triggersAndFunctionsFunctionsPage = new TriggersAndFunctionsFunctionsPage(); const libraryName = 'lib'; +const streamKeyName = 'StreamKey'; const filePathes = { upload: path.join('..', '..', '..', 'test-data', 'triggers-and-functions', 'library.txt'), @@ -168,3 +169,27 @@ test('Verify that function can be invoked', async t => { await t.expect(await triggersAndFunctionsFunctionsPage.Cli.getExecutedCommandTextByIndex()).eql(expectedCommand); await t.click(triggersAndFunctionsFunctionsPage.Cli.cliCollapseButton); }); + +test.after(async() => { + await browserPage.deleteKeyByNameFromList(streamKeyName); + await browserPage.Cli.sendCommandInCli(`TFUNCTION DELETE ${libraryName}`); + await deleteStandaloneDatabaseApi(ossStandaloneRedisGears); +})('Verify that user can open a Stream key from function', async t => { + const command1 = `#!js api_version=1.0 name=${libraryName}`; + const command2 = `redis.registerStreamTrigger('${LIBRARIES_LIST[3].name}', 'name', function(){});`; + + await browserPage.addStreamKey(streamKeyName, 'keyField', 'keyValue'); + await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.click(triggersAndFunctionsFunctionsPage.librariesLink); + await t.click(triggersAndFunctionsLibrariesPage.addLibraryButton); + await triggersAndFunctionsLibrariesPage.sendTextToMonaco(MonacoEditorInputs.Code, command1, command2); + await t.click(triggersAndFunctionsLibrariesPage.addLibrarySubmitButton); + await t.click(triggersAndFunctionsLibrariesPage.functionsLink); + await t.click(triggersAndFunctionsFunctionsPage.getFunctionsNameSelector(LIBRARIES_LIST[3].name)); + await t.click(triggersAndFunctionsFunctionsPage.invokeButton); + await t.typeText(triggersAndFunctionsFunctionsPage.keyNameStreamFunctions, `${streamKeyName}*`); + await t.click(triggersAndFunctionsFunctionsPage.findKeyButton); + await t.debug(); + await t.expect(await browserPage.isKeyIsDisplayedInTheList(streamKeyName)).ok('The stream key is not opened'); + await t.expect(browserPage.keyDetailsBadge.exists).ok('The key details is not opened'); +}); From 189270e2c63d5fe6d7308d95546000bd0ca5d6a5 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 10 Jul 2023 13:15:37 +0200 Subject: [PATCH 059/106] fixes by pr comments --- tests/e2e/helpers/api/api-common.ts | 69 ++++++--- tests/e2e/helpers/api/api-database.ts | 85 ++++++---- tests/e2e/helpers/api/api-info.ts | 9 +- tests/e2e/helpers/api/api-keys.ts | 214 +++++++++++++++++--------- tests/e2e/helpers/constants.ts | 9 +- 5 files changed, 247 insertions(+), 139 deletions(-) diff --git a/tests/e2e/helpers/api/api-common.ts b/tests/e2e/helpers/api/api-common.ts index 6a2959b06e..8c0805f1b9 100644 --- a/tests/e2e/helpers/api/api-common.ts +++ b/tests/e2e/helpers/api/api-common.ts @@ -1,46 +1,65 @@ import * as request from 'supertest'; import { Common } from '../common'; -import { Methods } from '../constants'; const endpoint = Common.getEndpoint(); +const jsonType = 'application/json'; /** - * Send request using API - * @param method http method + * Send GET request using API + * @param resourcePath URI path segment + */ +export async function sendGetRequest(resourcePath: string): Promise { + const windowId = Common.getWindowId(); + let requestEndpoint: any; + + requestEndpoint = request(endpoint) + .get(resourcePath) + .set('Accept', jsonType); + if (await windowId) { + requestEndpoint.set('X-Window-Id', await windowId); + } +} + +/** + * Send POST request using API * @param resourcePath URI path segment - * @param statusCode Expected status code of the response * @param body Request body */ -export async function sendRequest( - method: string, +export async function sendPostRequest( resourcePath: string, - statusCode: number, body?: Record ): Promise { const windowId = Common.getWindowId(); let requestEndpoint: any; - if (method === Methods.post) { - (requestEndpoint = request(endpoint) - .post(resourcePath) - .send(body) - .set('Accept', 'application/json')); - } - else if (method === Methods.get) { - (requestEndpoint = request(endpoint) - .get(resourcePath) - .set('Accept', 'application/json')); - } - else if (method === Methods.delete) { - (requestEndpoint = request(endpoint) - .delete(resourcePath) - .send(body) - .set('Accept', 'application/json')); - } + requestEndpoint = request(endpoint) + .post(resourcePath) + .send(body) + .set('Accept', jsonType); if (await windowId) { requestEndpoint.set('X-Window-Id', await windowId); } +} - return await requestEndpoint.expect(statusCode); +/** + * Send DELETE request using API + * @param resourcePath URI path segment + * @param body Request body + */ +export async function sendDeleteRequest( + resourcePath: string, + body?: Record +): Promise { + const windowId = Common.getWindowId(); + let requestEndpoint: any; + + requestEndpoint = request(endpoint) + .delete(resourcePath) + .send(body) + .set('Accept', jsonType); + + if (await windowId) { + requestEndpoint.set('X-Window-Id', await windowId); + } } diff --git a/tests/e2e/helpers/api/api-database.ts b/tests/e2e/helpers/api/api-database.ts index d546d4db3f..80a4aa6488 100644 --- a/tests/e2e/helpers/api/api-database.ts +++ b/tests/e2e/helpers/api/api-database.ts @@ -8,8 +8,12 @@ import { SentinelParameters, ClusterNodes, } from '../../pageObjects/components/myRedisDatabase/add-redis-database'; -import { Methods } from '../constants'; -import { sendRequest } from './api-common'; +import { ResourcePath } from '../constants'; +import { + sendGetRequest, + sendPostRequest, + sendDeleteRequest, +} from './api-common'; const chance = new Chance(); @@ -60,10 +64,8 @@ export class DatabaseAPIRequests { key: databaseParameters.clientCert!.key, }; } - const response = await sendRequest( - Methods.post, - '/databases', - 201, + const response = await sendPostRequest( + ResourcePath.Databases, requestBody ); await t @@ -72,6 +74,7 @@ export class DatabaseAPIRequests { databaseParameters.databaseName, `Database Name is not equal to ${databaseParameters.databaseName} in response` ); + await t.expect(await response.status).eql(201); } /** @@ -100,10 +103,8 @@ export class DatabaseAPIRequests { host: databaseParameters.ossClusterHost, port: Number(databaseParameters.ossClusterPort), }; - const response = await sendRequest( - Methods.post, - '/databases', - 201, + const response = await sendPostRequest( + ResourcePath.Databases, requestBody ); await t @@ -112,6 +113,7 @@ export class DatabaseAPIRequests { databaseParameters.ossClusterDatabaseName, `Database Name is not equal to ${databaseParameters.ossClusterDatabaseName} in response` ); + await t.expect(await response.status).eql(201); } /** @@ -133,20 +135,20 @@ export class DatabaseAPIRequests { password: databaseParameters.sentinelPassword, masters: masters, }; + const resourcePath = + ResourcePath.RedisSentinel + ResourcePath.Databases; + const response = await sendPostRequest(resourcePath, requestBody); - await sendRequest( - Methods.post, - '/redis-sentinel/databases', - 201, - requestBody - ); + await t.expect(await response.status).eql(201); } /** * Get all databases through api */ async getAllDatabases(): Promise { - const response = await sendRequest(Methods.get, '/databases', 200); + const response = await sendGetRequest(ResourcePath.Databases); + + await t.expect(await response.status).eql(200); return await response.body; } @@ -209,12 +211,11 @@ export class DatabaseAPIRequests { } if (databaseIds.length > 0) { const requestBody = { ids: databaseIds }; - await sendRequest( - Methods.delete, - '/databases', - 200, + const response = await sendDeleteRequest( + ResourcePath.Databases, requestBody ); + await t.expect(await response.status).eql(200); } } } @@ -231,7 +232,11 @@ export class DatabaseAPIRequests { ); if (databaseId) { const requestBody = { ids: [`${databaseId}`] }; - await sendRequest(Methods.delete, '/databases', 200, requestBody); + const response = await sendDeleteRequest( + ResourcePath.Databases, + requestBody + ); + await t.expect(await response.status).eql(200); } else { throw new Error('Error: Missing databaseId'); } @@ -248,12 +253,11 @@ export class DatabaseAPIRequests { const databaseId = await this.getDatabaseIdByName(databaseName); if (databaseId) { const requestBody = { ids: [`${databaseId}`] }; - await sendRequest( - Methods.delete, - '/databases', - 200, + const response = await sendDeleteRequest( + ResourcePath.Databases, requestBody ); + await t.expect(await response.status).eql(200); } else { throw new Error('Error: Missing databaseId'); } @@ -271,7 +275,11 @@ export class DatabaseAPIRequests { databaseParameters.ossClusterDatabaseName ); const requestBody = { ids: [`${databaseId}`] }; - await sendRequest(Methods.delete, '/databases', 200, requestBody); + const response = await sendDeleteRequest( + ResourcePath.Databases, + requestBody + ); + await t.expect(await response.status).eql(200); } /** @@ -286,7 +294,11 @@ export class DatabaseAPIRequests { databaseParameters.name![i] ); const requestBody = { ids: [`${databaseId}`] }; - await sendRequest(Methods.delete, '/databases', 200, requestBody); + const response = await sendDeleteRequest( + ResourcePath.Databases, + requestBody + ); + await t.expect(await response.status).eql(200); } } @@ -300,7 +312,11 @@ export class DatabaseAPIRequests { connectionType ); const requestBody = { ids: [`${databaseIds}`] }; - await sendRequest(Methods.delete, '/databases', 200, requestBody); + const response = await sendDeleteRequest( + ResourcePath.Databases, + requestBody + ); + await t.expect(await response.status).eql(200); } /** @@ -327,11 +343,12 @@ export class DatabaseAPIRequests { const databaseId = await this.getDatabaseIdByName( databaseParameters.ossClusterDatabaseName ); - const response = await sendRequest( - Methods.get, - `/databases/${databaseId}/cluster-details`, - 200 - ); + const resourcePath = + ResourcePath.Databases + databaseId + ResourcePath.ClusterDetails; + const response = await sendGetRequest(resourcePath); + + await t.expect(await response.status).eql(200); + const nodes = await response.body.nodes; const nodeNames = await nodes.map( (node: ClusterNodes) => `${node.host}:${node.port}` diff --git a/tests/e2e/helpers/api/api-info.ts b/tests/e2e/helpers/api/api-info.ts index 940f13fe75..7d41fdbd7f 100644 --- a/tests/e2e/helpers/api/api-info.ts +++ b/tests/e2e/helpers/api/api-info.ts @@ -1,9 +1,12 @@ -import { sendRequest } from './api-common'; -import { Methods } from '../constants'; +import { t } from 'testcafe'; +import { sendPostRequest } from './api-common'; +import { ResourcePath } from '../constants'; /** * Synchronize features */ export async function syncFeaturesApi(): Promise { - await sendRequest(Methods.post, '/features/sync', 200); + const response = await sendPostRequest(ResourcePath.SyncFeatures); + + await t.expect(await response.status).eql(200); } diff --git a/tests/e2e/helpers/api/api-keys.ts b/tests/e2e/helpers/api/api-keys.ts index 8d42940b01..04e3aaeea5 100644 --- a/tests/e2e/helpers/api/api-keys.ts +++ b/tests/e2e/helpers/api/api-keys.ts @@ -1,17 +1,15 @@ import { t } from 'testcafe'; -import * as request from 'supertest'; import { AddNewDatabaseParameters } from '../../pageObjects/components/myRedisDatabase/add-redis-database'; -import { Common } from '../../helpers/common'; import { HashKeyParameters, ListKeyParameters, SetKeyParameters, SortedSetKeyParameters, - StreamKeyParameters + StreamKeyParameters, } from '../../pageObjects/browser-page'; +import { sendGetRequest, sendPostRequest } from './api-common'; import { DatabaseAPIRequests } from './api-database'; -const endpoint = Common.getEndpoint(); const databaseAPIRequests = new DatabaseAPIRequests(); /** @@ -19,16 +17,25 @@ const databaseAPIRequests = new DatabaseAPIRequests(); * @param keyParameters The key parameters * @param databaseParameters The database parameters */ -export async function addHashKeyApi(keyParameters: HashKeyParameters, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); - const response = await request(endpoint).post(`/databases/${databaseId}/hash?encoding=buffer`) - .send({ - 'keyName': keyParameters.keyName, - 'fields': keyParameters.fields - }) - .set('Accept', 'application/json'); - - await t.expect(response.status).eql(201, 'The creation of new Hash key request failed'); +export async function addHashKeyApi( + keyParameters: HashKeyParameters, + databaseParameters: AddNewDatabaseParameters +): Promise { + const databaseId = await databaseAPIRequests.getDatabaseIdByName( + databaseParameters.databaseName + ); + const requestBody = { + keyName: keyParameters.keyName, + fields: keyParameters.fields, + }; + const response = await sendPostRequest( + `/databases/${databaseId}/hash?encoding=buffer`, + requestBody + ); + + await t + .expect(response.status) + .eql(201, 'The creation of new Hash key request failed'); } /** @@ -36,16 +43,25 @@ export async function addHashKeyApi(keyParameters: HashKeyParameters, databasePa * @param keyParameters The key parameters * @param databaseParameters The database parameters */ -export async function addStreamKeyApi(keyParameters: StreamKeyParameters, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); - const response = await request(endpoint).post(`/databases/${databaseId}/streams?encoding=buffer`) - .send({ - 'keyName': keyParameters.keyName, - 'entries': keyParameters.entries - }) - .set('Accept', 'application/json'); - - await t.expect(response.status).eql(201, 'The creation of new Stream key request failed'); +export async function addStreamKeyApi( + keyParameters: StreamKeyParameters, + databaseParameters: AddNewDatabaseParameters +): Promise { + const databaseId = await databaseAPIRequests.getDatabaseIdByName( + databaseParameters.databaseName + ); + const requestBody = { + keyName: keyParameters.keyName, + entries: keyParameters.entries, + }; + const response = await sendPostRequest( + `/databases/${databaseId}/streams?encoding=buffer`, + requestBody + ); + + await t + .expect(response.status) + .eql(201, 'The creation of new Stream key request failed'); } /** @@ -53,16 +69,25 @@ export async function addStreamKeyApi(keyParameters: StreamKeyParameters, databa * @param keyParameters The key parameters * @param databaseParameters The database parameters */ -export async function addSetKeyApi(keyParameters: SetKeyParameters, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); - const response = await request(endpoint).post(`/databases/${databaseId}/set?encoding=buffer`) - .send({ - 'keyName': keyParameters.keyName, - 'members': keyParameters.members - }) - .set('Accept', 'application/json'); - - await t.expect(response.status).eql(201, 'The creation of new Set key request failed'); +export async function addSetKeyApi( + keyParameters: SetKeyParameters, + databaseParameters: AddNewDatabaseParameters +): Promise { + const databaseId = await databaseAPIRequests.getDatabaseIdByName( + databaseParameters.databaseName + ); + const requestBody = { + keyName: keyParameters.keyName, + members: keyParameters.members, + }; + const response = await sendPostRequest( + `/databases/${databaseId}/set?encoding=buffer`, + requestBody + ); + + await t + .expect(response.status) + .eql(201, 'The creation of new Set key request failed'); } /** @@ -70,16 +95,25 @@ export async function addSetKeyApi(keyParameters: SetKeyParameters, databasePara * @param keyParameters The key parameters * @param databaseParameters The database parameters */ -export async function addSortedSetKeyApi(keyParameters: SortedSetKeyParameters, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); - const response = await request(endpoint).post(`/databases/${databaseId}/zSet?encoding=buffer`) - .send({ - 'keyName': keyParameters.keyName, - 'members': keyParameters.members - }) - .set('Accept', 'application/json'); - - await t.expect(response.status).eql(201, 'The creation of new Sorted Set key request failed'); +export async function addSortedSetKeyApi( + keyParameters: SortedSetKeyParameters, + databaseParameters: AddNewDatabaseParameters +): Promise { + const databaseId = await databaseAPIRequests.getDatabaseIdByName( + databaseParameters.databaseName + ); + const requestBody = { + keyName: keyParameters.keyName, + members: keyParameters.members, + }; + const response = await sendPostRequest( + `/databases/${databaseId}/zSet?encoding=buffer`, + requestBody + ); + + await t + .expect(response.status) + .eql(201, 'The creation of new Sorted Set key request failed'); } /** @@ -87,16 +121,25 @@ export async function addSortedSetKeyApi(keyParameters: SortedSetKeyParameters, * @param keyParameters The key parameters * @param databaseParameters The database parameters */ -export async function addListKeyApi(keyParameters: ListKeyParameters, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); - const response = await request(endpoint).post(`/databases/${databaseId}/list?encoding=buffer`) - .send({ - 'keyName': keyParameters.keyName, - 'element': keyParameters.element - }) - .set('Accept', 'application/json'); - - await t.expect(response.status).eql(201, 'The creation of new List key request failed'); +export async function addListKeyApi( + keyParameters: ListKeyParameters, + databaseParameters: AddNewDatabaseParameters +): Promise { + const databaseId = await databaseAPIRequests.getDatabaseIdByName( + databaseParameters.databaseName + ); + const requestBody = { + keyName: keyParameters.keyName, + element: keyParameters.element, + }; + const response = await sendPostRequest( + `/databases/${databaseId}/list?encoding=buffer`, + requestBody + ); + + await t + .expect(response.status) + .eql(201, 'The creation of new List key request failed'); } /** @@ -104,11 +147,18 @@ export async function addListKeyApi(keyParameters: ListKeyParameters, databasePa * @param keyName The key name * @param databaseParameters The database parameters */ -export async function searchKeyByNameApi(keyName: string, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); - const response = await request(endpoint).get(`/databases/${databaseId}/keys?cursor=0&count=5000&match=${keyName}`) - .set('Accept', 'application/json').expect(200); - +export async function searchKeyByNameApi( + keyName: string, + databaseParameters: AddNewDatabaseParameters +): Promise { + const databaseId = await databaseAPIRequests.getDatabaseIdByName( + databaseParameters.databaseName + ); + const response = await sendGetRequest( + `/databases/${databaseId}/keys?cursor=0&count=5000&match=${keyName}` + ); + + await t.expect(response.status).eql(200, 'Getting key request failed'); return await response.body[0].keys; } @@ -117,15 +167,24 @@ export async function searchKeyByNameApi(keyName: string, databaseParameters: Ad * @param keyName The key name * @param databaseParameters The database parameters */ -export async function deleteKeyByNameApi(keyName: string, databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); +export async function deleteKeyByNameApi( + keyName: string, + databaseParameters: AddNewDatabaseParameters +): Promise { + const databaseId = await databaseAPIRequests.getDatabaseIdByName( + databaseParameters.databaseName + ); const isKeyExist = await searchKeyByNameApi(keyName, databaseParameters); if (isKeyExist.length > 0) { - const response = await request(endpoint).delete(`/databases/${databaseId}/keys`) - .send({ 'keyNames': [keyName] }) - .set('Accept', 'application/json'); - - await t.expect(response.status).eql(200, 'The deletion of the key request failed'); + const requestBody = { keyNames: [keyName] }; + const response = await sendPostRequest( + `/databases/${databaseId}/keys`, + requestBody + ); + + await t + .expect(response.status) + .eql(200, 'The deletion of the key request failed'); } } @@ -134,11 +193,20 @@ export async function deleteKeyByNameApi(keyName: string, databaseParameters: Ad * @param keyNames The names of keys * @param databaseParameters The database parameters */ -export async function deleteKeysApi(keyNames: string[], databaseParameters: AddNewDatabaseParameters): Promise { - const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseParameters.databaseName); - const response = await request(endpoint).delete(`/databases/${databaseId}/keys`) - .send({ 'keyNames': keyNames }) - .set('Accept', 'application/json'); - - await t.expect(response.status).eql(200, 'The deletion of the keys request failed'); +export async function deleteKeysApi( + keyNames: string[], + databaseParameters: AddNewDatabaseParameters +): Promise { + const databaseId = await databaseAPIRequests.getDatabaseIdByName( + databaseParameters.databaseName + ); + const requestBody = { keyNames: keyNames }; + const response = await sendPostRequest( + `/databases/${databaseId}/keys`, + requestBody + ); + + await t + .expect(response.status) + .eql(200, 'The deletion of the keys request failed'); } diff --git a/tests/e2e/helpers/constants.ts b/tests/e2e/helpers/constants.ts index 17a4ebc232..61e71ecc96 100644 --- a/tests/e2e/helpers/constants.ts +++ b/tests/e2e/helpers/constants.ts @@ -48,8 +48,9 @@ export enum RecommendationIds { searchJson = 'searchJSON', } -export enum Methods { - post = 'post', - get = 'get', - delete = 'delete' +export enum ResourcePath { + Databases = '/databases', + RedisSentinel = '/redis-sentinel', + ClusterDetails = '/cluster-details', + SyncFeatures = '/features/sync', } From baede93a3ce05a98cbb296d2194246d4baee50d9 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 10 Jul 2023 13:23:19 +0200 Subject: [PATCH 060/106] removed tests for graph in workbench as outdated --- .../e2e/tests/regression/workbench/default-scripts-area.e2e.ts | 1 - .../e2e/tests/regression/workbench/redis-stack-commands.e2e.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts index ae8d9e0abd..2aa5da6dd6 100644 --- a/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts @@ -127,7 +127,6 @@ test('Verify that the same type of content is supported in the “Tutorials” a 'Working with JSON', 'Vector Similarity Search', 'Redis for time series', - 'Working with graphs', 'Probabilistic data structures' ]; const command = 'HSET bikes:10000 '; diff --git a/tests/e2e/tests/regression/workbench/redis-stack-commands.e2e.ts b/tests/e2e/tests/regression/workbench/redis-stack-commands.e2e.ts index 327c0454ab..81643dc801 100644 --- a/tests/e2e/tests/regression/workbench/redis-stack-commands.e2e.ts +++ b/tests/e2e/tests/regression/workbench/redis-stack-commands.e2e.ts @@ -41,7 +41,8 @@ test.skip await t.switchToIframe(workbenchPage.iframe); await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.queryGraphContainer).exists).ok('The Graph view is not switched for GRAPH command'); }); -test +//skipped due to Graph no longer displayed in tutorials +test.skip .meta({ env: env.desktop })('Verify that user can see "No data to visualize" message for Graph command', async t => { // Send Graph command await t.click(workbenchPage.redisStackTutorialsButton); From 121066f9f04eae095c572b547fcafde89dd7b752 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 10 Jul 2023 14:11:32 +0200 Subject: [PATCH 061/106] #RI-4599 - add triggers and functions to onboarding --- .../ui/src/components/config/Config.tsx | 17 ++++++-- .../navigation-menu/NavigationMenu.tsx | 1 + .../OnboardingFeatures.spec.tsx | 43 +++++++++++++++++++ .../OnboardingFeatures.tsx | 29 +++++++++++++ redisinsight/ui/src/constants/onboarding.ts | 2 + .../ui/src/pages/pubSub/PubSubPage.tsx | 36 +--------------- .../ui/src/pages/pubSub/styles.module.scss | 11 ----- .../TriggeredFunctionsPage.tsx | 30 ++++++++++++- .../triggeredFunctions/styles.modules.scss | 12 ++++++ 9 files changed, 132 insertions(+), 49 deletions(-) diff --git a/redisinsight/ui/src/components/config/Config.tsx b/redisinsight/ui/src/components/config/Config.tsx index 1fec3b36c9..d9c99a8c96 100644 --- a/redisinsight/ui/src/components/config/Config.tsx +++ b/redisinsight/ui/src/components/config/Config.tsx @@ -108,13 +108,24 @@ const Config = () => { } const onboardUsers = () => { - if (serverInfo?.buildType === BuildType.Electron && config) { + if (config) { const totalSteps = Object.keys(ONBOARDING_FEATURES).length const userCurrentStep = localStorageService.get(BrowserStorageItem.onboardingStep) - if (!config.agreements || isNumber(userCurrentStep)) { + // start onboarding for new electron users + if (serverInfo?.buildType === BuildType.Electron && !config.agreements) { + dispatch(setOnboarding({ + currentStep: 0, + totalSteps + })) + + return + } + + // continue onboarding for all users + if (isNumber(userCurrentStep)) { dispatch(setOnboarding({ - currentStep: config.agreements ? userCurrentStep : 0, + currentStep: userCurrentStep, totalSteps })) } diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx index b6eb7990b0..c23d6f6743 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx @@ -168,6 +168,7 @@ const NavigationMenu = () => { getIconType() { return this.isActivePage ? TriggeredFunctionsActiveSVG : TriggeredFunctionsSVG }, + onboard: ONBOARDING_FEATURES.TRIGGERED_FUNCTIONS_PAGE }, ] diff --git a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx index 64b054d009..9457d71d27 100644 --- a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx +++ b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx @@ -771,6 +771,49 @@ describe('ONBOARDING_FEATURES', () => { fireEvent.click(screen.getByTestId('back-btn')) expect(pushMock).toHaveBeenCalledWith(Pages.slowLog('')) }) + + it('should call proper history push on next ', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + fireEvent.click(screen.getByTestId('next-btn')) + expect(pushMock).toHaveBeenCalledWith(Pages.triggeredFunctions('')) + }) + }) + + describe('TRIGGERED_FUNCTIONS_PAGE', () => { + beforeEach(() => { + (appFeatureOnboardingSelector as jest.Mock).mockReturnValue({ + currentStep: OnboardingSteps.TriggeredFunctionsPage, + isActive: true, + totalSteps: Object.keys(ONBOARDING_FEATURES).length + }) + }) + + it('should render', () => { + expect( + render() + ).toBeTruthy() + expect(screen.getByTestId('step-content')).toHaveTextContent('Triggers and functions can execute server-side functions triggered by certain events or data') + }) + + it('should call proper telemetry events', () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + + render() + checkAllTelemetryButtons(OnboardingStepName.TriggeredFunctions, sendEventTelemetry as jest.Mock) + }) + + it('should properly push history on back', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + fireEvent.click(screen.getByTestId('back-btn')) + expect(pushMock).toHaveBeenCalledWith(Pages.pubSub('')) + }) }) describe('FINISH', () => { diff --git a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx index 7843096cb9..d393658cdb 100644 --- a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx +++ b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx @@ -460,6 +460,35 @@ const ONBOARDING_FEATURES = { history.push(Pages.slowLog(connectedInstanceId)) sendBackTelemetryEvent(...telemetryArgs) }, + onNext: () => { + history.push(Pages.triggeredFunctions(connectedInstanceId)) + sendNextTelemetryEvent(...telemetryArgs) + } + } + } + }, + TRIGGERED_FUNCTIONS_PAGE: { + step: OnboardingSteps.TriggeredFunctionsPage, + title: 'Triggers & Functions', + Inner: () => { + const { id: connectedInstanceId = '' } = useSelector(connectedInstanceSelector) + const history = useHistory() + const telemetryArgs: TelemetryArgs = [connectedInstanceId, OnboardingStepName.TriggeredFunctions] + + return { + content: ( + <> + Triggers and functions can execute server-side functions triggered by certain events or data + to decrease latency and react in real time to database events. + + See the list of uploaded libraries, upload or delete libraries, or investigate and debug functions. + + ), + onSkip: () => sendClosedTelemetryEvent(...telemetryArgs), + onBack: () => { + history.push(Pages.pubSub(connectedInstanceId)) + sendBackTelemetryEvent(...telemetryArgs) + }, onNext: () => sendNextTelemetryEvent(...telemetryArgs) } } diff --git a/redisinsight/ui/src/constants/onboarding.ts b/redisinsight/ui/src/constants/onboarding.ts index 4860437321..873d684ba7 100644 --- a/redisinsight/ui/src/constants/onboarding.ts +++ b/redisinsight/ui/src/constants/onboarding.ts @@ -15,6 +15,7 @@ enum OnboardingSteps { AnalyticsRecommendations, AnalyticsSlowLog, PubSubPage, + TriggeredFunctionsPage, Finish } @@ -36,6 +37,7 @@ enum OnboardingStepName { DatabaseAnalysisRecommendations = 'database_analysis_recommendations', SlowLog = 'slow_log', PubSub = 'pub_sub', + TriggeredFunctions = 'triggers_and_functions', Finish = 'finish', } diff --git a/redisinsight/ui/src/pages/pubSub/PubSubPage.tsx b/redisinsight/ui/src/pages/pubSub/PubSubPage.tsx index 987e1c9c5d..8dcdc8b11e 100644 --- a/redisinsight/ui/src/pages/pubSub/PubSubPage.tsx +++ b/redisinsight/ui/src/pages/pubSub/PubSubPage.tsx @@ -1,18 +1,14 @@ import { EuiTitle } from '@elastic/eui' import React, { useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { appAnalyticsInfoSelector } from 'uiSrc/slices/app/info' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import InstanceHeader from 'uiSrc/components/instance-header' import { SubscriptionType } from 'uiSrc/constants/pubSub' -import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry' +import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' -import { OnboardingTour } from 'uiSrc/components' -import { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features' -import { incrementOnboardStepAction } from 'uiSrc/slices/app/features' -import { OnboardingSteps } from 'uiSrc/constants/onboarding' import { MessagesListWrapper, PublishMessage, SubscriptionPanel } from './components' import styles from './styles.module.scss' @@ -29,25 +25,6 @@ const PubSubPage = () => { const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}` setTitle(`${dbName} - Pub/Sub`) - const dispatch = useDispatch() - - useEffect(() => () => { - // as here is the last step of onboarding, we set next step when move from the page - // remove it when pubSub won't be the last page - dispatch(incrementOnboardStepAction( - OnboardingSteps.Finish, - 0, - () => { - sendEventTelemetry({ - event: TelemetryEvent.ONBOARDING_TOUR_FINISHED, - eventData: { - databaseId: instanceId - } - }) - } - )) - }, []) - useEffect(() => { if (connectedInstanceName && !isPageViewSent && analyticsIdentified) { sendPageView(instanceId) @@ -80,15 +57,6 @@ const PubSubPage = () => {
-
- - - -
) diff --git a/redisinsight/ui/src/pages/pubSub/styles.module.scss b/redisinsight/ui/src/pages/pubSub/styles.module.scss index 752e6d646f..405ec8c051 100644 --- a/redisinsight/ui/src/pages/pubSub/styles.module.scss +++ b/redisinsight/ui/src/pages/pubSub/styles.module.scss @@ -44,14 +44,3 @@ } } -.onboardAnchor { - position: fixed; - visibility: hidden; - opacity: 0; -} - -.onboardPanel { - position: fixed; - top: calc(100% - 194px) !important; - left: calc(100% - 328px) !important; -} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx index 70e5af5b6a..47470fe551 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/TriggeredFunctionsPage.tsx @@ -6,13 +6,17 @@ import InstanceHeader from 'uiSrc/components/instance-header' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' +import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry' import { appAnalyticsInfoSelector } from 'uiSrc/slices/app/info' import { appContextTriggeredFunctions, setLastTriggeredFunctionsPage } from 'uiSrc/slices/app/context' +import { OnboardingTour } from 'uiSrc/components' +import { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features' +import { incrementOnboardStepAction } from 'uiSrc/slices/app/features' +import { OnboardingSteps } from 'uiSrc/constants/onboarding' import TriggeredFunctionsPageRouter from './TriggeredFunctionsPageRouter' import TriggeredFunctionsTabs from './components/TriggeredFunctionsTabs' @@ -40,6 +44,21 @@ const TriggeredFunctionsPage = ({ routes = [] }: Props) => { useEffect(() => () => { dispatch(setLastTriggeredFunctionsPage(pathnameRef.current)) + + // as here is the last step of onboarding, we set next step when move from the page + // remove it when triggers&functions won't be the last page + dispatch(incrementOnboardStepAction( + OnboardingSteps.Finish, + 0, + () => { + sendEventTelemetry({ + event: TelemetryEvent.ONBOARDING_TOUR_FINISHED, + eventData: { + databaseId: instanceId + } + }) + } + )) }, []) useEffect(() => { @@ -83,6 +102,15 @@ const TriggeredFunctionsPage = ({ routes = [] }: Props) => {
+
+ + + +
) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss b/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss index cc2f00ad98..afdcfb9277 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss +++ b/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss @@ -181,3 +181,15 @@ $breakpoint-to-hide-resize-panel: 1124px; } } } + +.onboardAnchor { + position: fixed; + visibility: hidden; + opacity: 0; +} + +.onboardPanel { + position: fixed; + top: calc(100% - 212px) !important; + left: calc(100% - 328px) !important; +} From 12c1af0899c3f6415dac1e6a190a264041ffe7e9 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 10 Jul 2023 14:20:19 +0200 Subject: [PATCH 062/106] fix --- tests/e2e/helpers/api/api-common.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e/helpers/api/api-common.ts b/tests/e2e/helpers/api/api-common.ts index 8c0805f1b9..81eed16088 100644 --- a/tests/e2e/helpers/api/api-common.ts +++ b/tests/e2e/helpers/api/api-common.ts @@ -18,6 +18,8 @@ export async function sendGetRequest(resourcePath: string): Promise { if (await windowId) { requestEndpoint.set('X-Window-Id', await windowId); } + + return requestEndpoint; } /** @@ -40,6 +42,7 @@ export async function sendPostRequest( if (await windowId) { requestEndpoint.set('X-Window-Id', await windowId); } + return requestEndpoint; } /** @@ -62,4 +65,6 @@ export async function sendDeleteRequest( if (await windowId) { requestEndpoint.set('X-Window-Id', await windowId); } + + return requestEndpoint; } From c22de91addc3c41e0abdc8bd01c940902ef80dd0 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 10 Jul 2023 15:11:15 +0200 Subject: [PATCH 063/106] fix of test --- tests/e2e/helpers/api/api-database.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/helpers/api/api-database.ts b/tests/e2e/helpers/api/api-database.ts index 80a4aa6488..c9f0c04527 100644 --- a/tests/e2e/helpers/api/api-database.ts +++ b/tests/e2e/helpers/api/api-database.ts @@ -344,7 +344,7 @@ export class DatabaseAPIRequests { databaseParameters.ossClusterDatabaseName ); const resourcePath = - ResourcePath.Databases + databaseId + ResourcePath.ClusterDetails; + ResourcePath.Databases + `/${databaseId}` + ResourcePath.ClusterDetails; const response = await sendGetRequest(resourcePath); await t.expect(await response.status).eql(200); From 9a3a79571e212e802e0a87cc0b564af81434a832 Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:27:13 +0300 Subject: [PATCH 064/106] #RI-4596 - update condition for luaToFunctions recommendation (#2303) --- .../api/src/common/constants/recommendations.ts | 2 +- .../strategies/lua-to-functions.strategy.spec.ts | 14 +++++++------- .../providers/recommendation.provider.spec.ts | 6 +++--- .../POST-databases-id-analysis.test.ts | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/redisinsight/api/src/common/constants/recommendations.ts b/redisinsight/api/src/common/constants/recommendations.ts index d17d1d6346..90a63e3c85 100644 --- a/redisinsight/api/src/common/constants/recommendations.ts +++ b/redisinsight/api/src/common/constants/recommendations.ts @@ -23,4 +23,4 @@ export const COMBINE_SMALL_STRINGS_TO_HASHES_RECOMMENDATION_KEYS_COUNT = 10; export const SEARCH_HASH_RECOMMENDATION_KEYS_FOR_CHECK = 50; export const SEARCH_HASH_RECOMMENDATION_KEYS_LENGTH = 2; export const RTS_KEYS_FOR_CHECK = 100; -export const LUA_TO_FUNCTIONS_RECOMMENDATION_COUNT = 1; +export const LUA_TO_FUNCTIONS_RECOMMENDATION_COUNT = 0; diff --git a/redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.spec.ts b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.spec.ts index 53d242dcbf..b6bf91f74a 100644 --- a/redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.spec.ts +++ b/redisinsight/api/src/modules/database-recommendation/scanner/strategies/lua-to-functions.strategy.spec.ts @@ -37,7 +37,7 @@ describe('LuaToFunctionsStrategy', () => { databaseService.get.mockResolvedValue({ modules: [{ name: 'redisgears' }] }); }); - it('should return true when there is more then 1 lua script', async () => { + it('should return true when there is more then 0 lua script', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'TFUNCTION' })) .mockResolvedValue(mockEmptyLibraries); @@ -45,11 +45,11 @@ describe('LuaToFunctionsStrategy', () => { expect(await strategy.isRecommendationReached({ client: nodeClient, databaseId: mockDatabaseId, - info: { cashedScripts: 2 }, + info: { cashedScripts: 1 }, })).toEqual({ isReached: true }); }); - it('should return false when number of cached lua script is 1', async () => { + it('should return false when number of cached lua script is 0', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'TFUNCTION' })) .mockResolvedValue(mockEmptyLibraries); @@ -57,7 +57,7 @@ describe('LuaToFunctionsStrategy', () => { expect(await strategy.isRecommendationReached({ client: nodeClient, databaseId: mockDatabaseId, - info: { cashedScripts: 1 }, + info: { cashedScripts: 0 }, })).toEqual({ isReached: false }); }); @@ -82,15 +82,15 @@ describe('LuaToFunctionsStrategy', () => { expect(await strategy.isRecommendationReached({ client: nodeClient, databaseId: mockDatabaseId, - info: { cashedScripts: 2 }, + info: { cashedScripts: 1 }, })).toEqual({ isReached: true }); }); - it('should return false when number of cached lua script is 1', async () => { + it('should return false when number of cached lua script is 0', async () => { expect(await strategy.isRecommendationReached({ client: nodeClient, databaseId: mockDatabaseId, - info: { cashedScripts: 1 }, + info: { cashedScripts: 0 }, })).toEqual({ isReached: false }); }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index b0f662a702..72a3c6759a 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -10,8 +10,8 @@ nodeClient.sendCommand = jest.fn(); const mockRedisMemoryInfoResponse1: string = '# Memory\r\nnumber_of_cached_scripts:10\r\n'; const mockRedisMemoryInfoResponse2: string = '# Memory\r\nnumber_of_cached_scripts:11\r\n'; -const mockRedisMemoryInfoResponse3: string = '# Memory\r\nnumber_of_cached_scripts:1\r\n'; -const mockRedisMemoryInfoResponse4: string = '# Memory\r\nnumber_of_cached_scripts:2\r\n'; +const mockRedisMemoryInfoResponse3: string = '# Memory\r\nnumber_of_cached_scripts:0\r\n'; +const mockRedisMemoryInfoResponse4: string = '# Memory\r\nnumber_of_cached_scripts:1\r\n'; const mockRedisKeyspaceInfoResponse1: string = '# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n'; const mockRedisKeyspaceInfoResponse2: string = `# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n @@ -674,7 +674,7 @@ describe('RecommendationProvider', () => { expect(luaToFunctionsRecommendation).toEqual(null); }); - it('should return luaToFunctions recommendation when lua script > 1', async () => { + it('should return luaToFunctions recommendation when lua script > 0', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'info' })) .mockResolvedValueOnce(mockRedisMemoryInfoResponse4); diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 4157f320d3..2112971c20 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -689,7 +689,7 @@ describe('POST /databases/:instanceId/analysis', () => { statusCode: 201, responseSchema, before: async () => { - await rte.data.generateNCachedScripts(2, true); + await rte.data.generateNCachedScripts(1, true); }, checkFn: async ({ body }) => { expect(body.recommendations).to.include.deep.members([ From 595f0c0b81f4aac16c12bb5ae08ad65b907bfee6 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Mon, 10 Jul 2023 16:58:29 +0200 Subject: [PATCH 065/106] fix foc comments --- .../tests/critical-path/triggers-and-functions/libraries.e2e.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index 3336a856ad..b73bf98b60 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -189,7 +189,6 @@ test.after(async() => { await t.click(triggersAndFunctionsFunctionsPage.invokeButton); await t.typeText(triggersAndFunctionsFunctionsPage.keyNameStreamFunctions, `${streamKeyName}*`); await t.click(triggersAndFunctionsFunctionsPage.findKeyButton); - await t.debug(); await t.expect(await browserPage.isKeyIsDisplayedInTheList(streamKeyName)).ok('The stream key is not opened'); await t.expect(browserPage.keyDetailsBadge.exists).ok('The key details is not opened'); }); From 43d60d9672de7bd46f79e5a0e5343529cf811ea8 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 10 Jul 2023 17:19:23 +0200 Subject: [PATCH 066/106] skip outdated test on graph in command helper --- tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts b/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts index 316fbae06a..33c5b58541 100644 --- a/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts +++ b/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts @@ -94,7 +94,8 @@ test // await browserPage.CommandHelper.checkURLCommand(timeSeriesCommands[0], `https://redis.io/commands/${timeSeriesCommands[0].toLowerCase()}/`); // await t.switchToParentWindow(); }); -test +// outdated after https://redislabs.atlassian.net/browse/RI-4608 +test.skip .meta({ env: env.web })('Verify that user can type GRAPH. in Command helper and see auto-suggestions from RedisGraph commands.json', async t => { const commandForSearch = 'GRAPH.'; // const externalPageLink = 'https://redis.io/commands/graph.config-get/'; From 1b68a9f07ebe8668da429353957e2d615d336a67 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 11 Jul 2023 08:46:22 +0200 Subject: [PATCH 067/106] #RI-4599 - update text --- .../src/components/onboarding-features/OnboardingFeatures.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx index d393658cdb..128370032d 100644 --- a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx +++ b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx @@ -478,8 +478,8 @@ const ONBOARDING_FEATURES = { return { content: ( <> - Triggers and functions can execute server-side functions triggered by certain events or data - to decrease latency and react in real time to database events. + Triggers and Functions can execute server-side functions triggered by certain + events or data operations to decrease latency and react in real time to database events. See the list of uploaded libraries, upload or delete libraries, or investigate and debug functions. From 5a1d8b14b035a1cfc973688578dc42bbb2468faa Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Tue, 11 Jul 2023 09:14:09 +0200 Subject: [PATCH 068/106] fix for comments#2 --- .../critical-path/triggers-and-functions/libraries.e2e.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index b73bf98b60..45ab54baa9 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -10,13 +10,14 @@ import { FunctionsSections, LibrariesSections, MonacoEditorInputs, rte } from '. import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { TriggersAndFunctionLibrary } from '../../../interfaces/triggers-and-functions'; import { CommonElementsActions } from '../../../common-actions/common-elements-actions'; +import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); const triggersAndFunctionsLibrariesPage = new TriggersAndFunctionsLibrariesPage(); const triggersAndFunctionsFunctionsPage = new TriggersAndFunctionsFunctionsPage(); -const libraryName = 'lib'; -const streamKeyName = 'StreamKey'; +const libraryName = Common.generateWord(5); +const streamKeyName = Common.generateWord(5); const filePathes = { upload: path.join('..', '..', '..', 'test-data', 'triggers-and-functions', 'library.txt'), From 20885e74abcc53380fb925f2c4f7d2d5c1512b92 Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Tue, 11 Jul 2023 10:22:39 +0300 Subject: [PATCH 069/106] #RI-4726, 4727 - fix utm in the links (#2307) * #RI-4726, 4727 - fix utm in the links * #RI-4726 - capitalized triggers and functions --- .../constants/dbAnalysisRecommendations.json | 54 +++++++++---------- .../recommendations-view/Recommendations.tsx | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 2a2cd0a301..57eebf0ebd 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -400,7 +400,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -544,7 +544,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -705,7 +705,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -983,7 +983,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -1016,7 +1016,7 @@ { "type": "link", "value": { - "href": "https://docs.redis.com/latest/modules/redisearch/redisearch-active-active/", + "href": "https://docs.redis.com/latest/stack/search/search-active-active/", "name": "Active-Active setup" } }, @@ -1075,7 +1075,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -1108,7 +1108,7 @@ { "type": "link", "value": { - "href": "https://docs.redis.com/latest/modules/redisearch/redisearch-active-active/", + "href": "https://docs.redis.com/latest/stack/search/search-active-active/", "name": "Active-Active" } }, @@ -1204,7 +1204,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -1264,7 +1264,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -1385,7 +1385,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -1418,7 +1418,7 @@ { "type": "link", "value": { - "href": "https://docs.redis.com/latest/modules/redisearch/redisearch-active-active/", + "href": "https://docs.redis.com/latest/stack/search/search-active-active/", "name": "Active-Active setup" } }, @@ -1439,12 +1439,12 @@ }, "luaToFunctions": { "id": "luaToFunctions", - "title": "Consider using triggers and functions", + "title": "Consider using Triggers and Functions", "tutorial": "/quick-guides/triggers-and-functions/introduction.md", "content": [ { "type": "paragraph", - "value": "If you are using LUA scripts to run application logic inside Redis, consider using triggers and functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." + "value": "If you are using LUA scripts to run application logic inside Redis, consider using Triggers and Functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." }, { "type": "spacer", @@ -1452,7 +1452,7 @@ }, { "type": "paragraph", - "value": "Triggers and functions can execute business logic on changes within a database, and read across all shards in clustered databases." + "value": "Triggers and Functions can execute business logic on changes within a database, and read across all shards in clustered databases." }, { "type": "spacer", @@ -1465,7 +1465,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -1500,12 +1500,12 @@ }, "functionsWithStreams": { "id": "functionsWithStreams", - "title": "Consider using triggers and functions to react in real-time to stream entries", + "title": "Consider using Triggers and Functions to react in real-time to stream entries", "tutorial": "/quick-guides/triggers-and-functions/introduction.md", "content": [ { "type": "paragraph", - "value": "If you need to manipulate your data based on Redis stream entries, consider using stream triggers that are a part of triggers and functions. It can help lower latency by moving business logic closer to the data." + "value": "If you need to manipulate your data based on Redis stream entries, consider using stream triggers that are a part of Triggers and Functions. It can help lower latency by moving business logic closer to the data." }, { "type": "spacer", @@ -1513,7 +1513,7 @@ }, { "type": "paragraph", - "value": "Try triggers and functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." + "value": "Try Triggers and Functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." }, { "type": "spacer", @@ -1529,12 +1529,12 @@ }, { "type": "span", - "value": "Triggers and functions are part of " + "value": "Triggers and Functions are part of " }, { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -1570,19 +1570,19 @@ }, { "type": "paragraph", - "value": "Try the interactive tutorial to learn more about triggers and functions." + "value": "Try the interactive tutorial to learn more about Triggers and Functions." } ], "badges": ["code_changes"] }, "functionsWithKeyspace": { "id": "functionsWithKeyspace", - "title": "Consider using triggers and functions to react in real-time to database changes", + "title": "Consider using Triggers and Functions to react in real-time to database changes", "tutorial": "/quick-guides/triggers-and-functions/introduction.md", "content": [ { "type": "paragraph", - "value": "If you need to manipulate your data based on keyspace notifications, consider using keyspace triggers that are a part of triggers and functions. It can help lower latency by moving business logic closer to the data." + "value": "If you need to manipulate your data based on keyspace notifications, consider using keyspace triggers that are a part of Triggers and Functions. It can help lower latency by moving business logic closer to the data." }, { "type": "spacer", @@ -1590,7 +1590,7 @@ }, { "type": "paragraph", - "value": "Try triggers and functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." + "value": "Try Triggers and Functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." }, { "type": "spacer", @@ -1606,12 +1606,12 @@ }, { "type": "span", - "value": "Triggers and functions are part of " + "value": "Triggers and Functions are part of " }, { "type": "link", "value": { - "href": "https://redis.io/docs/stack/about/", + "href": "https://redis.io/docs/about/about-stack/", "name": "Redis Stack" } }, @@ -1647,7 +1647,7 @@ }, { "type": "paragraph", - "value": "Try the interactive tutorial to learn more about triggers and functions." + "value": "Try the interactive tutorial to learn more about Triggers and Functions." } ], "badges": ["code_changes"] diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 7c5caa7e50..d70142fcd3 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -187,7 +187,7 @@ const Recommendations = () => { data-testid={`${id}-accordion`} > - {renderRecommendationContent(content, params, telemetryEvent ?? name)} + {renderRecommendationContent(content, params, { telemetryName: telemetryEvent ?? name })} {!!params?.keys?.length && ( Date: Tue, 11 Jul 2023 15:03:34 +0700 Subject: [PATCH 070/106] #RI-4727 - update recommendation links --- .../constants/dbAnalysisRecommendations.json | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 57eebf0ebd..9e04c58fc0 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -29,7 +29,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/manual/programmability/", + "href": "https://redis.io/docs/interact/programmability/", "name": "documentation" } }, @@ -389,7 +389,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/search/", + "href": "https://redis.io/docs/interact/search-and-query/", "name": "query and search capabilities" } }, @@ -525,7 +525,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/bloom/", + "href": "https://redis.io/docs/data-types/probabilistic/bloom-filter/", "name": "probabilistic data structures" } }, @@ -670,7 +670,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/timeseries/", + "href": "https://redis.io/docs/data-types/timeseries/", "name": "time series capabilities" } }, @@ -817,7 +817,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/search/", + "href": "https://redis.io/docs/interact/search-and-query/", "name": "query and search" } }, @@ -828,7 +828,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/json/", + "href": "https://redis.io/docs/data-types/json/", "name": "JSON" } }, @@ -839,7 +839,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/bloom/", + "href": "https://redis.io/docs/data-types/probabilistic/bloom-filter/", "name": "probabilistic data structures" } }, @@ -850,7 +850,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/timeseries/", + "href": "https://redis.io/docs/data-types/timeseries/", "name": "Time Series" } }, @@ -871,7 +871,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/search/", + "href": "https://redis.io/docs/interact/search-and-query/", "name": "RediSearch" } }, @@ -964,7 +964,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/search/", + "href": "https://redis.io/docs/interact/search-and-query/", "name": "query and search capabilities" } }, @@ -1048,7 +1048,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/search/", + "href": "https://redis.io/docs/interact/search-and-query/", "name": "query and search capabilities" } }, @@ -1161,7 +1161,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/json/path/", + "href": "https://redis.io/docs/data-types/json/path/", "name": "JSONPath" } }, @@ -1190,7 +1190,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/search/", + "href": "https://redis.io/docs/interact/search-and-query/", "name": "query and search capabilities" } } @@ -1275,7 +1275,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/json/", + "href": "https://redis.io/docs/data-types/json/", "name": "JSON" } }, @@ -1286,7 +1286,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/search/", + "href": "https://redis.io/docs/interact/search-and-query/", "name": "query and search" } }, @@ -1297,7 +1297,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/timeseries/", + "href": "https://redis.io/docs/data-types/timeseries/", "name": "Time Series" } }, @@ -1340,7 +1340,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/search/reference/aggregations/#apply-expressions/", + "href": "https://redis.io/docs/interact/search-and-query/search/aggregations/", "name": "Apply Functions" } }, @@ -1358,7 +1358,7 @@ { "type": "link", "value": { - "href": "https://redis.io/docs/stack/search/", + "href": "https://redis.io/docs/interact/search-and-query/", "name": "query and search" } }, From 7edfa1599780dd592a7044f57158d58ce99e0202 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 11 Jul 2023 10:59:24 +0200 Subject: [PATCH 071/106] #RI-4600 - add highlighting for triggers & functions page --- .../navigation-menu/NavigationMenu.tsx | 22 ++++++++++++++++--- .../ui/src/constants/featuresHighlighting.tsx | 10 ++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx index c23d6f6743..0eca6685ea 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react' import { useHistory, useLocation } from 'react-router-dom' import cx from 'classnames' import { last } from 'lodash' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { EuiBadge, EuiButtonIcon, @@ -12,13 +12,13 @@ import { EuiPageSideBar, EuiToolTip } from '@elastic/eui' -import HighlightedFeature from 'uiSrc/components/hightlighted-feature/HighlightedFeature' +import HighlightedFeature, { Props as HighlightedFeatureProps } from 'uiSrc/components/hightlighted-feature/HighlightedFeature' import { ANALYTICS_ROUTES, TRIGGERED_FUNCTIONS_ROUTES } from 'uiSrc/components/main-router/constants/sub-routes' import { PageNames, Pages } from 'uiSrc/constants' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' import { getRouterLinkProps } from 'uiSrc/services' -import { appFeaturePagesHighlightingSelector } from 'uiSrc/slices/app/features' +import { appFeaturePagesHighlightingSelector, removeFeatureFromHighlighting } from 'uiSrc/slices/app/features' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { appInfoSelector, @@ -41,6 +41,7 @@ import Divider from 'uiSrc/components/divider/Divider' import { BuildType } from 'uiSrc/constants/env' import { renderOnboardingTourWithChild } from 'uiSrc/utils/onboarding' import { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features' +import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting' import HelpMenu from './components/help-menu/HelpMenu' import NotificationMenu from './components/notifications-center' @@ -68,6 +69,7 @@ interface INavigations { const NavigationMenu = () => { const history = useHistory() const location = useLocation() + const dispatch = useDispatch() const [activePage, setActivePage] = useState(Pages.home) @@ -89,6 +91,18 @@ const NavigationMenu = () => { ({ path }) => (`/${last(path.split('/'))}` === activePage) ) + const getAdditionPropsForHighlighting = (pageName: string): Omit => { + if (BUILD_FEATURES[pageName]?.asPageFeature) { + return ({ + hideFirstChild: true, + onClick: () => dispatch(removeFeatureFromHighlighting(pageName)), + ...BUILD_FEATURES[pageName] + }) + } + + return {} + } + const privateRoutes: INavigations[] = [ { tooltipText: 'Browser', @@ -209,9 +223,11 @@ const NavigationMenu = () => { {renderOnboardingTourWithChild( ( diff --git a/redisinsight/ui/src/constants/featuresHighlighting.tsx b/redisinsight/ui/src/constants/featuresHighlighting.tsx index ce134d818e..d91fe5de49 100644 --- a/redisinsight/ui/src/constants/featuresHighlighting.tsx +++ b/redisinsight/ui/src/constants/featuresHighlighting.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { PageNames } from 'uiSrc/constants/pages' export type FeaturesHighlightingType = 'plain' | 'tooltip' | 'popover' @@ -7,8 +8,15 @@ interface BuildHighlightingFeature { title?: string | React.ReactElement content?: string | React.ReactElement page?: string + asPageFeature?: boolean } export const BUILD_FEATURES: Record = { - + [PageNames.triggeredFunctions]: { + type: 'tooltip', + title: 'Triggers & Functions', + content: 'Triggers and Functions can execute server-side functions triggered by events or data operations to decrease latency and react in real time to database events.', + page: PageNames.triggeredFunctions, + asPageFeature: true + } } as const From 2c66392bc589fe89b4721203ddd5dca0edf2b2ad Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Tue, 11 Jul 2023 12:31:15 +0200 Subject: [PATCH 072/106] fix tests for 4502 --- .../critical-path/triggers-and-functions/libraries.e2e.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index d58e964e48..1aa500058d 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -138,6 +138,7 @@ test('Verify that library and functions can be deleted', async t => { test('Verify that library can be uploaded', async t => { const configuration = '{"redisgears_2.lock-redis-timeout": 1000}'; const functionNameFromFile = 'function'; + const libNameFromFile = 'lib'; await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); await t.click(triggersAndFunctionsFunctionsPage.librariesLink); @@ -148,15 +149,16 @@ test('Verify that library can be uploaded', async t => { await CommonElementsActions.checkCheckbox(triggersAndFunctionsLibrariesPage.addConfigurationCheckBox, true); await triggersAndFunctionsLibrariesPage.sendTextToMonaco(MonacoEditorInputs.Configuration, configuration); await t.click(await triggersAndFunctionsLibrariesPage.addLibrarySubmitButton); - await t.expect(triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libraryName).exists).ok('the library was not added'); + await t.expect(triggersAndFunctionsLibrariesPage.getLibraryNameSelector(libNameFromFile).exists).ok('the library was not added'); await t.expect(triggersAndFunctionsLibrariesPage.getFunctionsByName(LibrariesSections.Functions, functionNameFromFile).exists).ok('the library information was not opened'); }); test('Verify that function can be invoked', async t => { const functionNameFromFile = 'function'; + const libNameFromFile = 'lib'; const keyName = ['Hello']; const argumentsName = ['world', '!!!' ]; - const expectedCommand = `TFCALL "${libraryName}.${functionNameFromFile}" "${keyName.length}" "${keyName}" "${argumentsName.join('" "')}"`; + const expectedCommand = `TFCALL "${libNameFromFile}.${functionNameFromFile}" "${keyName.length}" "${keyName}" "${argumentsName.join('" "')}"`; await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); await t.click(triggersAndFunctionsFunctionsPage.librariesLink); From ae18987163e90ca90e946e349ecb960e084f1b34 Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Tue, 11 Jul 2023 14:04:53 +0300 Subject: [PATCH 073/106] #RI-4586 - align BE and FE getModules summary (#2313) * #RI-4586 - align BE and FE getModules summary --- .../api/src/constants/redis-modules.ts | 11 +++++ .../src/utils/redis-modules-summary.spec.ts | 3 +- .../api/src/utils/redis-modules-summary.ts | 49 ++++++++++++++++--- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/redisinsight/api/src/constants/redis-modules.ts b/redisinsight/api/src/constants/redis-modules.ts index d77f41de03..3cfdb13ccf 100644 --- a/redisinsight/api/src/constants/redis-modules.ts +++ b/redisinsight/api/src/constants/redis-modules.ts @@ -6,6 +6,7 @@ export enum AdditionalRedisModuleName { RedisJSON = 'ReJSON', RediSearch = 'search', RedisTimeSeries = 'timeseries', + 'Triggers & Functions' = 'redisgears' } export enum AdditionalSearchModuleName { @@ -14,6 +15,11 @@ export enum AdditionalSearchModuleName { FTL = 'ftl', } +export enum AdditionalTriggersAndFunctionsModuleName { + TriggersAndFunctions = 'redisgears', + TriggersAndFunctions2 = 'redisgears_2', +} + export const SUPPORTED_REDIS_MODULES = Object.freeze({ ai: AdditionalRedisModuleName.RedisAI, graph: AdditionalRedisModuleName.RedisGraph, @@ -60,3 +66,8 @@ export const REDISEARCH_MODULES: string[] = [ AdditionalSearchModuleName.FT, AdditionalSearchModuleName.FTL, ] + +export const TRIGGERED_AND_FUNCTIONS_MODULES: string[] = [ + AdditionalTriggersAndFunctionsModuleName.TriggersAndFunctions, + AdditionalTriggersAndFunctionsModuleName.TriggersAndFunctions2, +] diff --git a/redisinsight/api/src/utils/redis-modules-summary.spec.ts b/redisinsight/api/src/utils/redis-modules-summary.spec.ts index fad97545a0..966352972b 100644 --- a/redisinsight/api/src/utils/redis-modules-summary.spec.ts +++ b/redisinsight/api/src/utils/redis-modules-summary.spec.ts @@ -9,6 +9,7 @@ const DEFAULT_SUMMARY = Object.freeze( RedisBloom: { loaded: false }, RedisJSON: { loaded: false }, RedisTimeSeries: { loaded: false }, + 'Triggers & Functions': { loaded: false }, customModules: [], }, ); @@ -53,7 +54,7 @@ const getRedisModulesSummaryTests = [ RedisJSON: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RediSearch: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RedisTimeSeries: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, - 'Triggers & Functios': { loaded: true, version: 10000, semanticVersion: '1.0.0' }, + 'Triggers & Functions': { loaded: true, version: 10000, semanticVersion: '1.0.0' }, customModules: [], }, }, diff --git a/redisinsight/api/src/utils/redis-modules-summary.ts b/redisinsight/api/src/utils/redis-modules-summary.ts index b61a6c403f..fd56f4ccf9 100644 --- a/redisinsight/api/src/utils/redis-modules-summary.ts +++ b/redisinsight/api/src/utils/redis-modules-summary.ts @@ -1,5 +1,10 @@ import { cloneDeep } from 'lodash'; -import { AdditionalRedisModuleName, SUPPORTED_REDIS_MODULES } from 'src/constants'; +import { + AdditionalRedisModuleName, + SUPPORTED_REDIS_MODULES, + REDISEARCH_MODULES, + TRIGGERED_AND_FUNCTIONS_MODULES, +} from 'src/constants'; import { AdditionalRedisModule } from 'src/modules/database/models/additional.redis.module'; interface IModuleSummary { @@ -19,30 +24,58 @@ export const DEFAULT_SUMMARY: IRedisModulesSummary = Object.freeze( RedisBloom: { loaded: false }, RedisJSON: { loaded: false }, RedisTimeSeries: { loaded: false }, + 'Triggers & Functions': { loaded: false }, customModules: [], }, ); +export const isRedisearchAvailable = (modules: AdditionalRedisModule[]): boolean => ( + modules?.some(({ name }) => REDISEARCH_MODULES.some((search) => name === search)) +); + +export const isTriggeredAndFunctionsAvailable = (modules: AdditionalRedisModule[]): boolean => ( + modules?.some(({ name }) => TRIGGERED_AND_FUNCTIONS_MODULES.some((value) => name === value)) +); + const getEnumKeyBValue = (myEnum: any, enumValue: number | string): string => { const keys = Object.keys(myEnum); const index = keys.findIndex((x) => myEnum[x] === enumValue); return index > -1 ? keys[index] : ''; }; +const getModuleSummaryToSent = (module: AdditionalRedisModule) => ({ + loaded: true, + version: module.version, + semanticVersion: module.semanticVersion, +}); + +// same function as in FE export const getRedisModulesSummary = (modules: AdditionalRedisModule[] = []): IRedisModulesSummary => { const summary = cloneDeep(DEFAULT_SUMMARY); try { modules.forEach(((module) => { if (SUPPORTED_REDIS_MODULES[module.name]) { const moduleName = getEnumKeyBValue(AdditionalRedisModuleName, module.name); - summary[moduleName] = { - loaded: true, - version: module.version, - semanticVersion: module.semanticVersion, - }; - } else { - summary.customModules.push(module); + summary[moduleName] = getModuleSummaryToSent(module); + return; } + + if (isRedisearchAvailable([module])) { + const redisearchName = getEnumKeyBValue(AdditionalRedisModuleName, AdditionalRedisModuleName.RediSearch); + summary[redisearchName] = getModuleSummaryToSent(module); + return; + } + + if (isTriggeredAndFunctionsAvailable([module])) { + const triggeredAndFunctionsName = getEnumKeyBValue( + AdditionalRedisModuleName, + AdditionalRedisModuleName['Triggers & Functions'], + ); + summary[triggeredAndFunctionsName] = getModuleSummaryToSent(module); + return; + } + + summary.customModules.push(module); })); } catch (e) { // continue regardless of error From b24e1f1924acf9d9495b8c40497f529c8ba1f0f1 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 12 Jul 2023 13:16:46 +0700 Subject: [PATCH 074/106] #RI-4632 - add cluster refresh in triggers and functions --- .../dto/delete-library.dto.ts | 2 + .../triggered-functions.controller.ts | 14 ++-- .../triggered-functions.service.spec.ts | 34 ++++++++++ .../triggered-functions.service.ts | 43 ++++++++++-- .../DELETE-databases-id-library.test.ts | 62 +++++++++++++++++ .../GET-databases-id-functions.test.ts | 38 +++++++++++ .../GET-databases-id-libraries.test.ts | 27 ++++++++ .../POST-databases-id-library.test.ts | 67 +++++++++++++++++++ redisinsight/api/test/helpers/constants.ts | 4 ++ redisinsight/api/test/helpers/data/redis.ts | 13 ++++ .../test-runs/gears-clu/docker-compose.yml | 5 ++ 11 files changed, 295 insertions(+), 14 deletions(-) create mode 100644 redisinsight/api/test/api/triggered-functions/DELETE-databases-id-library.test.ts create mode 100644 redisinsight/api/test/api/triggered-functions/GET-databases-id-functions.test.ts create mode 100644 redisinsight/api/test/api/triggered-functions/GET-databases-id-libraries.test.ts create mode 100644 redisinsight/api/test/api/triggered-functions/POST-databases-id-library.test.ts create mode 100644 redisinsight/api/test/test-runs/gears-clu/docker-compose.yml diff --git a/redisinsight/api/src/modules/triggered-functions/dto/delete-library.dto.ts b/redisinsight/api/src/modules/triggered-functions/dto/delete-library.dto.ts index 3b18c68b2e..80375d399a 100644 --- a/redisinsight/api/src/modules/triggered-functions/dto/delete-library.dto.ts +++ b/redisinsight/api/src/modules/triggered-functions/dto/delete-library.dto.ts @@ -1,6 +1,7 @@ import { IsNotEmpty, IsString, + IsDefined, } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; @@ -9,6 +10,7 @@ export class DeleteLibraryDto { description: 'Library name', type: String, }) + @IsDefined() @IsString() @IsNotEmpty() libraryName: string; diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts index 071a16f125..18722c6217 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.controller.ts @@ -10,7 +10,7 @@ import { TriggeredFunctionsService } from 'src/modules/triggered-functions/trigg import { ShortLibrary, Library, Function } from 'src/modules/triggered-functions/models'; import { LibraryDto, UploadLibraryDto, DeleteLibraryDto } from 'src/modules/triggered-functions/dto'; import { ClientMetadata } from 'src/common/models'; -import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ClientMetadataParam } from 'src/common/decorators'; @ApiTags('Triggered Functions') @Controller('triggered-functions') @@ -31,7 +31,7 @@ export class TriggeredFunctionsController { ], }) async libraryList( - @BrowserClientMetadata() clientMetadata: ClientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, ): Promise { return this.service.libraryList(clientMetadata); } @@ -49,7 +49,7 @@ export class TriggeredFunctionsController { ], }) async details( - @BrowserClientMetadata() clientMetadata: ClientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, @Body() dto: LibraryDto, ): Promise { return this.service.details(clientMetadata, dto.libraryName); @@ -68,7 +68,7 @@ export class TriggeredFunctionsController { ], }) async functionsList( - @BrowserClientMetadata() clientMetadata: ClientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, ): Promise { return this.service.functionsList(clientMetadata); } @@ -79,7 +79,7 @@ export class TriggeredFunctionsController { statusCode: 201, }) async upload( - @BrowserClientMetadata() clientMetadata: ClientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, @Body() dto: UploadLibraryDto, ): Promise { return this.service.upload(clientMetadata, dto); @@ -91,7 +91,7 @@ export class TriggeredFunctionsController { statusCode: 201, }) async upgrade( - @BrowserClientMetadata() clientMetadata: ClientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, @Body() dto: UploadLibraryDto, ): Promise { return this.service.upload(clientMetadata, dto, true); @@ -103,7 +103,7 @@ export class TriggeredFunctionsController { description: 'Delete library by name', }) async deleteLibraries( - @BrowserClientMetadata() clientMetadata: ClientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, // library name probably can be really huge @Body() dto: DeleteLibraryDto, ): Promise { diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts index 6a796d5f3c..9acd4a20f6 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.spec.ts @@ -2,9 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { plainToClass } from 'class-transformer'; import { ForbiddenException, NotFoundException } from '@nestjs/common'; import { + mockIOClusterNode1, + mockIOClusterNode2, mockClientMetadata, mockDatabaseConnectionService, mockIORedisClient, + mockIORedisCluster, mockVerboseLibraryReply, mockSimpleLibraryReply, MockType, @@ -306,6 +309,15 @@ describe('TriggeredFunctionsService', () => { expect(e).toBeInstanceOf(NotFoundException); } }); + + it('should call refresh cluster', async () => { + databaseConnectionService.getOrCreateClient.mockResolvedValueOnce(mockIORedisCluster); + const refreshClusterSpy = jest.spyOn(service as any, 'refreshCluster'); + refreshClusterSpy.mockResolvedValue(null); + + await service.upload(mockClientMetadata, { code: mockCode, configuration: mockConfig }, true); + expect(refreshClusterSpy).toHaveBeenCalled(); + }); }); describe('delete', () => { @@ -344,5 +356,27 @@ describe('TriggeredFunctionsService', () => { expect(e).toBeInstanceOf(NotFoundException); } }); + + it('should call refresh cluster', async () => { + databaseConnectionService.getOrCreateClient.mockResolvedValueOnce(mockIORedisCluster); + const refreshClusterSpy = jest.spyOn(service as any, 'refreshCluster'); + refreshClusterSpy.mockResolvedValue(null); + + await service.delete(mockClientMetadata, mockLibraryName); + expect(refreshClusterSpy).toHaveBeenCalled(); + }); + }); + + describe('refreshCluster', () => { + it('should call REDISGEARS_2.REFRESHCLUSTER on each shard', async () => { + mockIORedisCluster.sendCommand.mockResolvedValue(null); + await service['refreshCluster'](mockIORedisCluster); + + expect(mockIORedisCluster.nodes).toBeCalledTimes(1); + expect(mockIOClusterNode1.sendCommand) + .toBeCalledWith(jasmine.objectContaining({ name: 'REDISGEARS_2.REFRESHCLUSTER' })); + expect(mockIOClusterNode2.sendCommand) + .toBeCalledWith(jasmine.objectContaining({ name: 'REDISGEARS_2.REFRESHCLUSTER' })); + }); }); }); diff --git a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts index 4d2404ba0d..9302632a22 100644 --- a/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts +++ b/redisinsight/api/src/modules/triggered-functions/triggered-functions.service.ts @@ -1,4 +1,4 @@ -import { Command } from 'ioredis'; +import { Command, Redis, Cluster } from 'ioredis'; import { HttpException, Injectable, Logger, NotFoundException, } from '@nestjs/common'; @@ -60,12 +60,12 @@ export class TriggeredFunctionsService { clientMetadata: ClientMetadata, name: string, ): Promise { - let client; + let client: Redis | Cluster; try { client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); const reply = await client.sendCommand( new Command('TFUNCTION', ['LIST', 'WITHCODE', 'LIBRARY', name], { replyEncoding: 'utf8' }), - ); + ) as string[][]; if (!reply.length) { this.logger.error( @@ -98,12 +98,12 @@ export class TriggeredFunctionsService { async functionsList( clientMetadata: ClientMetadata, ): Promise { - let client; + let client: Redis | Cluster; try { client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); const reply = await client.sendCommand( new Command('TFUNCTION', ['LIST', 'vvv'], { replyEncoding: 'utf8' }), - ); + ) as any; const functions = reply.reduce((prev, cur) => concat(prev, getLibraryFunctions(cur)), []); return functions.map((func) => plainToClass( Function, @@ -131,7 +131,7 @@ export class TriggeredFunctionsService { dto: UploadLibraryDto, isExist = false, ): Promise { - let client; + let client: Redis | Cluster; try { const { code, configuration, @@ -145,6 +145,11 @@ export class TriggeredFunctionsService { commandArgs.push(code); client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); + + if (client.isCluster) { + await this.refreshCluster(client); + } + await client.sendCommand( new Command('TFUNCTION', [...commandArgs], { replyEncoding: 'utf8' }), ); @@ -172,10 +177,14 @@ export class TriggeredFunctionsService { clientMetadata: ClientMetadata, libraryName: string, ): Promise { - let client; + let client: Redis | Cluster; try { client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); + if (client.isCluster) { + await this.refreshCluster(client); + } + await client.sendCommand( new Command('TFUNCTION', ['DELETE', libraryName], { replyEncoding: 'utf8' }), ); @@ -193,4 +202,24 @@ export class TriggeredFunctionsService { throw catchAclError(e); } } + + /** + * On oss cluster, before executing any gears function, + * you must send REDISGEARS_2.REFRESHCLUSTER command to all the shards + * so that all the shards will be aware of the cluster topology. + * + * @param client + * @private + */ + private async refreshCluster( + client, + ): Promise { + const nodes = client.nodes('master'); + + await Promise.all(nodes.map(async (node) => { + await node.sendCommand( + new Command('REDISGEARS_2.REFRESHCLUSTER'), + ); + })); + } } diff --git a/redisinsight/api/test/api/triggered-functions/DELETE-databases-id-library.test.ts b/redisinsight/api/test/api/triggered-functions/DELETE-databases-id-library.test.ts new file mode 100644 index 0000000000..a3d4dbd209 --- /dev/null +++ b/redisinsight/api/test/api/triggered-functions/DELETE-databases-id-library.test.ts @@ -0,0 +1,62 @@ +import { + Joi, + expect, + describe, + before, + deps, + generateInvalidDataTestCases, + validateInvalidDataTestCase, getMainCheckFn, + requirements, +} from '../deps'; + +const { request, server, constants, rte } = deps; + + +const endpoint = ( + instanceId = constants.TEST_INSTANCE_ID, +) => + request(server).delete(`/${constants.API.DATABASES}/${instanceId}/triggered-functions/library`); + +// input data schema +const dataSchema = Joi.object({ + libraryName: Joi.string().required(), +}).strict(); + +const validInputData = { + libraryName: constants.getRandomString(), +}; + +const mainCheckFn = getMainCheckFn(endpoint); + +describe(`DELETE /databases/:id/triggered-functions/library`, () => { + requirements('rte.modules.redisgears_2'); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + before(async () => { + await rte.data.generateTriggeredFunctionsLibrary() + }); + + [ + { + name: 'Should remove library by library name', + data: { + libraryName: constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME, + }, + before: async () => { + const libraries = await rte.data.sendCommand('TFUNCTION', ['LIST', 'LIBRARY', constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME]); + expect(libraries.length).to.eq(1); + }, + after: async () => { + const libraries = await rte.data.sendCommand('TFUNCTION', ['LIST', 'LIBRARY', constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME]); + expect(libraries.length).to.eq(0); + }, + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/api/test/api/triggered-functions/GET-databases-id-functions.test.ts b/redisinsight/api/test/api/triggered-functions/GET-databases-id-functions.test.ts new file mode 100644 index 0000000000..253b8c5b32 --- /dev/null +++ b/redisinsight/api/test/api/triggered-functions/GET-databases-id-functions.test.ts @@ -0,0 +1,38 @@ +import { describe, deps, requirements, _, getMainCheckFn } from '../deps'; +import { Joi } from '../../helpers/test'; +const { request, server, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).get(`/${constants.API.DATABASES}/${instanceId}/triggered-functions/functions`); + +const responseSchema = Joi.array().items(Joi.object({ + type: Joi.string().valid('functions', 'cluster_functions', 'keyspace_triggers', 'stream_triggers').required(), + name: Joi.string().required(), + library: Joi.string(), + success: Joi.number(), + fail: Joi.number(), + total: Joi.number(), + flags: Joi.array().items(Joi.string()), + isAsync: Joi.boolean(), + description: Joi.string(), + lastError: Joi.string(), + lastExecutionTime: Joi.number(), + totalExecutionTime: Joi.number(), + prefix: Joi.string(), + trim: Joi.boolean(), + window: Joi.number() +})).required().strict(true); + +const mainCheckFn = getMainCheckFn(endpoint); + +describe(`GET /databases/:instanceId/history`, () => { + requirements('rte.modules.redisgears_2'); + + [ + { + name: 'Should get triggered functions libraries', + responseSchema, + }, + ].map(mainCheckFn); +}); diff --git a/redisinsight/api/test/api/triggered-functions/GET-databases-id-libraries.test.ts b/redisinsight/api/test/api/triggered-functions/GET-databases-id-libraries.test.ts new file mode 100644 index 0000000000..62adeb9fba --- /dev/null +++ b/redisinsight/api/test/api/triggered-functions/GET-databases-id-libraries.test.ts @@ -0,0 +1,27 @@ +import { describe, deps, requirements, _, getMainCheckFn } from '../deps'; +import { Joi } from '../../helpers/test'; +const { request, server, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).get(`/${constants.API.DATABASES}/${instanceId}/triggered-functions/libraries`); + +const responseSchema = Joi.array().items(Joi.object({ + name: Joi.string().required(), + user: Joi.string().required(), + totalFunctions: Joi.number().required(), + pendingJobs: Joi.number().required(), +})).required().strict(true); + +const mainCheckFn = getMainCheckFn(endpoint); + +describe(`GET /databases/:instanceId/history`, () => { + requirements('rte.modules.redisgears_2'); + + [ + { + name: 'Should get triggered functions libraries', + responseSchema, + }, + ].map(mainCheckFn); +}); diff --git a/redisinsight/api/test/api/triggered-functions/POST-databases-id-library.test.ts b/redisinsight/api/test/api/triggered-functions/POST-databases-id-library.test.ts new file mode 100644 index 0000000000..e040952da9 --- /dev/null +++ b/redisinsight/api/test/api/triggered-functions/POST-databases-id-library.test.ts @@ -0,0 +1,67 @@ +import { + expect, + describe, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + getMainCheckFn, _, +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).post(`/${constants.API.DATABASES}/${instanceId}/triggered-functions/library`); + +// input data schema +const dataSchema = Joi.object({ + code: Joi.string().required(), + configuration: Joi.string().allow(null), +}).strict(); + +const validInputData = { + code: constants.TEST_TRIGGERED_FUNCTIONS_CODE, + configuration: constants.TEST_TRIGGERED_FUNCTIONS_CONFIGURATION, +}; + +const mainCheckFn = getMainCheckFn(endpoint); + +describe('POST /databases/:instanceId/triggered-functions/library', () => { + requirements('rte.modules.redisgears_2'); + + describe('Main', () => { + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + [ + { + name: 'Should upload library', + data: { + code: constants.TEST_TRIGGERED_FUNCTIONS_CODE, + configuration: constants.TEST_TRIGGERED_FUNCTIONS_CONFIGURATION, + }, + statusCode: 201, + before: async () => { + // Triggered and functions did not have ability to remove all libraries + try { + await rte.data.sendCommand('TFUNCTION', ['DELETE', constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME]); + } catch (err) { + // ignore + } + const libraries = await rte.data.sendCommand('TFUNCTION', ['LIST', 'LIBRARY', constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME]); + expect(libraries.length).to.eq(0); + }, + after: async () => { + const libraries = await rte.data.sendCommand('TFUNCTION', ['LIST', 'LIBRARY', constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME]); + expect(libraries.length).to.eq(1); + }, + }, + ].map(mainCheckFn); + }); + }); +}); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 954f12db9a..7937030b4a 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -19,6 +19,7 @@ const APP_DEFAULT_SETTINGS = { theme: null, agreements: null, }; +const TEST_LIBRARY_NAME = 'lib'; const unprintableBuf = Buffer.concat([ Buffer.from('acedae', 'hex'), @@ -586,5 +587,8 @@ export const constants = { type: null, match: 'hi', }, + TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME: TEST_LIBRARY_NAME, + TEST_TRIGGERED_FUNCTIONS_CODE: `#!js api_version=1.0 name=${TEST_LIBRARY_NAME}\n redis.registerFunction('foo', ()=>{return 'bar'})`, + TEST_TRIGGERED_FUNCTIONS_CONFIGURATION: "{}", // etc... } diff --git a/redisinsight/api/test/helpers/data/redis.ts b/redisinsight/api/test/helpers/data/redis.ts index 637368b326..2d8a9d4d80 100644 --- a/redisinsight/api/test/helpers/data/redis.ts +++ b/redisinsight/api/test/helpers/data/redis.ts @@ -542,6 +542,18 @@ export const initDataHelper = (rte) => { return executeCommand(...command.split(' ')); }; + const generateTriggeredFunctionsLibrary = async (clean: boolean = true): Promise => { + if (clean) { + await truncate(); + } + + await sendCommand('TFUNCTION', [ + 'LOAD', + constants.TEST_TRIGGERED_FUNCTIONS_CODE, + constants.TEST_TRIGGERED_FUNCTIONS_CONFIGURATION, + ]) + } + return { sendCommand, executeCommand, @@ -568,6 +580,7 @@ export const initDataHelper = (rte) => { generateNCachedScripts, generateHugeNumberOfMembersForSetKey, getClientNodes, + generateTriggeredFunctionsLibrary, setRedisearchConfig, } } diff --git a/redisinsight/api/test/test-runs/gears-clu/docker-compose.yml b/redisinsight/api/test/test-runs/gears-clu/docker-compose.yml new file mode 100644 index 0000000000..711280c9cb --- /dev/null +++ b/redisinsight/api/test/test-runs/gears-clu/docker-compose.yml @@ -0,0 +1,5 @@ +version: "3" + +services: + redis: + image: redislabs/redisgears:edge From cb11166a6ce6aa23f129040df3aafd17683f875c Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Wed, 12 Jul 2023 11:03:37 +0200 Subject: [PATCH 075/106] onboarding --- tests/e2e/tests/regression/browser/onboarding.e2e.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/regression/browser/onboarding.e2e.ts b/tests/e2e/tests/regression/browser/onboarding.e2e.ts index 0cfb562115..435ca60fb5 100644 --- a/tests/e2e/tests/regression/browser/onboarding.e2e.ts +++ b/tests/e2e/tests/regression/browser/onboarding.e2e.ts @@ -10,7 +10,7 @@ import { WorkbenchPage, PubSubPage, MyRedisDatabasePage, - BrowserPage + BrowserPage, TriggersAndFunctionsFunctionsPage } from '../../../pageObjects'; import { Telemetry } from '../../../helpers/telemetry'; import { OnboardingCardsDialog } from '../../../pageObjects/dialogs'; @@ -22,6 +22,7 @@ const memoryEfficiencyPage = new MemoryEfficiencyPage(); const workBenchPage = new WorkbenchPage(); const slowLogPage = new SlowLogPage(); const pubSubPage = new PubSubPage(); +const functionsPage = new TriggersAndFunctionsFunctionsPage(); const telemetry = new Telemetry(); const databaseHelper = new DatabaseHelper(); @@ -44,7 +45,7 @@ fixture `Onboarding new user tests` }); // https://redislabs.atlassian.net/browse/RI-4070, https://redislabs.atlassian.net/browse/RI-4067 // https://redislabs.atlassian.net/browse/RI-4278 -test('Verify onboarding new user steps', async t => { +test.only('Verify onboarding new user steps', async t => { await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton); await t.expect(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterPanel.visible).ok('help center panel is not opened'); // Verify that user can reset onboarding @@ -110,6 +111,10 @@ test('Verify onboarding new user steps', async t => { await t.expect(pubSubPage.subscribeButton.visible).ok('pub/sub page is not opened'); await onboardingCardsDialog.verifyStepVisible('Pub/Sub'); await onboardingCardsDialog.clickNextStep(); + // verify triggered and functions page is opened + await t.expect(functionsPage.librariesLink.visible).ok('triggered and functions page is not opened'); + await onboardingCardsDialog.verifyStepVisible('Triggers & Functions'); + await onboardingCardsDialog.clickNextStep(); // verify last step of onboarding process is visible await onboardingCardsDialog.verifyStepVisible('Great job!'); await onboardingCardsDialog.clickNextStep(); From 509906e1b9646fdf75056f62ea35a6e26ce995c0 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Wed, 12 Jul 2023 11:56:25 +0200 Subject: [PATCH 076/106] comments fix --- tests/e2e/tests/regression/browser/onboarding.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/regression/browser/onboarding.e2e.ts b/tests/e2e/tests/regression/browser/onboarding.e2e.ts index 435ca60fb5..ffa08d2f37 100644 --- a/tests/e2e/tests/regression/browser/onboarding.e2e.ts +++ b/tests/e2e/tests/regression/browser/onboarding.e2e.ts @@ -45,7 +45,7 @@ fixture `Onboarding new user tests` }); // https://redislabs.atlassian.net/browse/RI-4070, https://redislabs.atlassian.net/browse/RI-4067 // https://redislabs.atlassian.net/browse/RI-4278 -test.only('Verify onboarding new user steps', async t => { +test('Verify onboarding new user steps', async t => { await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton); await t.expect(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterPanel.visible).ok('help center panel is not opened'); // Verify that user can reset onboarding From f1917bba60e09f2ec5f1eb489ba35207782ec6c0 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 12 Jul 2023 19:09:47 +0700 Subject: [PATCH 077/106] #RI-4737 - add uppercase --- .../src/components/command-helper/CommandHelperWrapper.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx index 901abf57e9..6077bbfadf 100644 --- a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx +++ b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx @@ -34,9 +34,14 @@ const CommandHelperWrapper = () => { } = useSelector(cliSettingsSelector) const { spec: ALL_REDIS_COMMANDS, commandsArray } = useSelector(appRedisCommandsSelector) const { instanceId = '' } = useParams<{ instanceId: string }>() - const lastMatchedCommand = (isEnteringCommand && matchedCommand && !checkDeprecatedModuleCommand(matchedCommand)) + const lastMatchedCommand = ( + isEnteringCommand + && matchedCommand + && !checkDeprecatedModuleCommand(matchedCommand.toUpperCase()) + ) ? matchedCommand : searchedCommand + const KEYS_OF_COMMANDS = useMemo(() => removeDeprecatedModuleCommands(commandsArray), [commandsArray]) let searchedCommands: string[] = [] From 25c007c30a87e3a861b2e7a9d6b5d528ee094a3b Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 12 Jul 2023 19:13:29 +0700 Subject: [PATCH 078/106] #RI-4737 - add uppercase --- .../src/components/command-helper/CommandHelperWrapper.tsx | 6 +----- redisinsight/ui/src/utils/cliHelper.tsx | 2 +- redisinsight/ui/src/utils/tests/cliHelper.spec.ts | 1 + 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx index 6077bbfadf..28c76f1d4b 100644 --- a/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx +++ b/redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx @@ -34,11 +34,7 @@ const CommandHelperWrapper = () => { } = useSelector(cliSettingsSelector) const { spec: ALL_REDIS_COMMANDS, commandsArray } = useSelector(appRedisCommandsSelector) const { instanceId = '' } = useParams<{ instanceId: string }>() - const lastMatchedCommand = ( - isEnteringCommand - && matchedCommand - && !checkDeprecatedModuleCommand(matchedCommand.toUpperCase()) - ) + const lastMatchedCommand = (isEnteringCommand && matchedCommand && !checkDeprecatedModuleCommand(matchedCommand)) ? matchedCommand : searchedCommand diff --git a/redisinsight/ui/src/utils/cliHelper.tsx b/redisinsight/ui/src/utils/cliHelper.tsx index 43927d78d5..3bc6ce08b6 100644 --- a/redisinsight/ui/src/utils/cliHelper.tsx +++ b/redisinsight/ui/src/utils/cliHelper.tsx @@ -227,7 +227,7 @@ const DEPRECATED_MODULE_GROUPS = [ ] const checkDeprecatedModuleCommand = (command: string) => - DEPRECATED_MODULE_PREFIXES.some((prefix) => command.startsWith(prefix)) + DEPRECATED_MODULE_PREFIXES.some((prefix) => command.toUpperCase().startsWith(prefix)) const checkDeprecatedCommandGroup = (item: string) => DEPRECATED_MODULE_GROUPS.some((group) => group === item) diff --git a/redisinsight/ui/src/utils/tests/cliHelper.spec.ts b/redisinsight/ui/src/utils/tests/cliHelper.spec.ts index 2ad752bd99..8f185c9c08 100644 --- a/redisinsight/ui/src/utils/tests/cliHelper.spec.ts +++ b/redisinsight/ui/src/utils/tests/cliHelper.spec.ts @@ -116,6 +116,7 @@ const checkDeprecatedModuleCommandTests = [ { input: 'FT.foo bar', expected: false }, { input: 'GRAPH foo bar', expected: false }, { input: 'GRAPH.foo bar', expected: true }, + { input: 'graph.foo bar', expected: true }, { input: 'FOO bar', expected: false }, ] From f451ba024167b7eb92adb54f0a0c3cd37e76e680 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 12 Jul 2023 15:55:08 +0200 Subject: [PATCH 079/106] #RI-4715 - update styles for insight panel --- .../components/recommendation/styles.module.scss | 10 +++++----- .../recommendation-voting/styles.module.scss | 4 ---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss index d83d7e2475..f8be289736 100644 --- a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss +++ b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss @@ -81,7 +81,7 @@ background-color: var(--euiColorLightestShade); border-radius: 4px; padding: 3px 6px; - + code { font-size: inherit !important; } @@ -109,18 +109,18 @@ justify-content: space-around; align-items: center; border-top: 1px solid var(--separatorColorLight); - height: 40px; + height: 48px; .btn { - height: 24px !important; + height: 32px !important; min-width: 60px !important; :global(.euiButton__content) { - padding: 0 10px; + padding: 0 12px; } :global(.euiButton__text) { - font: normal normal 400 12px/14px Graphik, sans-serif !important; + font: normal normal 400 14px/17px Graphik, sans-serif !important; } &:last-of-type { diff --git a/redisinsight/ui/src/components/recommendation-voting/styles.module.scss b/redisinsight/ui/src/components/recommendation-voting/styles.module.scss index f9f0f08379..1a14e2bf9c 100644 --- a/redisinsight/ui/src/components/recommendation-voting/styles.module.scss +++ b/redisinsight/ui/src/components/recommendation-voting/styles.module.scss @@ -28,7 +28,3 @@ } } } - -div.highlightText { - color: var(--euiColorPrimary) !important; -} From 407257ea46e034c1f198e2c75beb0e561c7bee6e Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 13 Jul 2023 13:13:12 +0700 Subject: [PATCH 080/106] #RI-4624 - change learn more link --- redisinsight/desktop/src/lib/menu/menu.ts | 4 ++-- redisinsight/desktop/src/lib/tray/tray.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/redisinsight/desktop/src/lib/menu/menu.ts b/redisinsight/desktop/src/lib/menu/menu.ts index fd56d982c8..27401b2b43 100644 --- a/redisinsight/desktop/src/lib/menu/menu.ts +++ b/redisinsight/desktop/src/lib/menu/menu.ts @@ -196,7 +196,7 @@ export class MenuBuilder { { label: 'Learn More', click() { - shell.openExternal('https://docs.redis.com/latest/ri/') + shell.openExternal('https://redis.io/docs/ui/insight/?utm_source=redisinsight&utm_medium=main&utm_campaign=learn_more') } } ] @@ -313,7 +313,7 @@ export class MenuBuilder { { label: 'Learn More', click() { - shell.openExternal('https://docs.redis.com/latest/ri/') + shell.openExternal('https://redis.io/docs/ui/insight/?utm_source=redisinsight&utm_medium=main&utm_campaign=learn_more') } }, { type: 'separator' }, diff --git a/redisinsight/desktop/src/lib/tray/tray.ts b/redisinsight/desktop/src/lib/tray/tray.ts index 000eb5650d..90f195fae4 100644 --- a/redisinsight/desktop/src/lib/tray/tray.ts +++ b/redisinsight/desktop/src/lib/tray/tray.ts @@ -67,7 +67,7 @@ export class TrayBuilder { { label: 'Learn More', click() { - shell.openExternal('https://docs.redis.com/latest/ri/') + shell.openExternal('https://redis.io/docs/ui/insight/?utm_source=redisinsight&utm_medium=main&utm_campaign=learn_more') } }, { type: 'separator' }, From 9b7c6c99e587973f860b9324b98cd22f4162923c Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 17 Jul 2023 09:35:04 +0200 Subject: [PATCH 081/106] #RI-4739 - update icon --- .../ui/src/assets/img/modules/RedisGears2Dark.svg | 14 ++++++++++++++ .../ui/src/assets/img/modules/RedisGears2Light.svg | 14 ++++++++++++++ .../database-list-modules/DatabaseListModules.tsx | 6 ++++-- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 redisinsight/ui/src/assets/img/modules/RedisGears2Dark.svg create mode 100644 redisinsight/ui/src/assets/img/modules/RedisGears2Light.svg diff --git a/redisinsight/ui/src/assets/img/modules/RedisGears2Dark.svg b/redisinsight/ui/src/assets/img/modules/RedisGears2Dark.svg new file mode 100644 index 0000000000..67096901b5 --- /dev/null +++ b/redisinsight/ui/src/assets/img/modules/RedisGears2Dark.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/modules/RedisGears2Light.svg b/redisinsight/ui/src/assets/img/modules/RedisGears2Light.svg new file mode 100644 index 0000000000..89c854b6fe --- /dev/null +++ b/redisinsight/ui/src/assets/img/modules/RedisGears2Light.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx b/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx index 191eb35fe1..afe2c7ab78 100644 --- a/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx +++ b/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx @@ -18,6 +18,8 @@ import RedisBloomLight from 'uiSrc/assets/img/modules/RedisBloomLight.svg' import RedisBloomDark from 'uiSrc/assets/img/modules/RedisBloomDark.svg' import RedisGearsLight from 'uiSrc/assets/img/modules/RedisGearsLight.svg' import RedisGearsDark from 'uiSrc/assets/img/modules/RedisGearsDark.svg' +import RedisGears2Light from 'uiSrc/assets/img/modules/RedisGears2Light.svg' +import RedisGears2Dark from 'uiSrc/assets/img/modules/RedisGears2Dark.svg' import RedisGraphLight from 'uiSrc/assets/img/modules/RedisGraphLight.svg' import RedisGraphDark from 'uiSrc/assets/img/modules/RedisGraphDark.svg' import RedisJSONLight from 'uiSrc/assets/img/modules/RedisJSONLight.svg' @@ -75,8 +77,8 @@ export const modulesDefaultInit = { text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.RedisGears], }, [RedisDefaultModules.RedisGears2]: { - iconDark: RedisGearsDark, - iconLight: RedisGearsLight, + iconDark: RedisGears2Dark, + iconLight: RedisGears2Light, text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.RedisGears2], }, [RedisDefaultModules.ReJSON]: { From 8225eec784e8d98478d57c2e1528819fd43e65a5 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 17 Jul 2023 15:35:58 +0200 Subject: [PATCH 082/106] #RI-4758 - update triggers & functions icon for sidebar --- redisinsight/ui/src/assets/img/sidebar/gears.svg | 15 +++++++++++++-- .../ui/src/assets/img/sidebar/gears_active.svg | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/redisinsight/ui/src/assets/img/sidebar/gears.svg b/redisinsight/ui/src/assets/img/sidebar/gears.svg index 1ae540a6ad..810004ea7a 100644 --- a/redisinsight/ui/src/assets/img/sidebar/gears.svg +++ b/redisinsight/ui/src/assets/img/sidebar/gears.svg @@ -1,3 +1,14 @@ - - + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/sidebar/gears_active.svg b/redisinsight/ui/src/assets/img/sidebar/gears_active.svg index a5d38d44c2..b99b947172 100644 --- a/redisinsight/ui/src/assets/img/sidebar/gears_active.svg +++ b/redisinsight/ui/src/assets/img/sidebar/gears_active.svg @@ -1,3 +1,14 @@ - - + + + + + + + + + + + + + From 700c95760fff8a64ffeceffdd3f565d350ccff9d Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 18 Jul 2023 14:53:09 +0300 Subject: [PATCH 083/106] #RI-4718 - fix unhandled error issue in recommendations --- .../database-recommendation.service.ts | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/redisinsight/api/src/modules/database-recommendation/database-recommendation.service.ts b/redisinsight/api/src/modules/database-recommendation/database-recommendation.service.ts index 7693b91203..a2a74aa063 100644 --- a/redisinsight/api/src/modules/database-recommendation/database-recommendation.service.ts +++ b/redisinsight/api/src/modules/database-recommendation/database-recommendation.service.ts @@ -61,28 +61,33 @@ export class DatabaseRecommendationService { recommendationName: string, data: any, ): Promise { - const newClientMetadata = { - ...clientMetadata, - db: clientMetadata.db ?? (await this.databaseService.get(clientMetadata.databaseId))?.db ?? 0, - }; - const isRecommendationExist = await this.databaseRecommendationRepository.isExist( - newClientMetadata, - recommendationName, - ); - if (!isRecommendationExist) { - const recommendation = await this.scanner.determineRecommendation(recommendationName, data); - - if (recommendation) { - const entity = plainToClass( - DatabaseRecommendation, - { databaseId: newClientMetadata?.databaseId, ...recommendation }, - ); - - return await this.create(newClientMetadata, entity); + try { + const newClientMetadata = { + ...clientMetadata, + db: clientMetadata.db ?? (await this.databaseService.get(clientMetadata.databaseId))?.db ?? 0, + }; + const isRecommendationExist = await this.databaseRecommendationRepository.isExist( + newClientMetadata, + recommendationName, + ); + if (!isRecommendationExist) { + const recommendation = await this.scanner.determineRecommendation(recommendationName, data); + + if (recommendation) { + const entity = plainToClass( + DatabaseRecommendation, + { databaseId: newClientMetadata?.databaseId, ...recommendation }, + ); + + return await this.create(newClientMetadata, entity); + } } - } - return null; + return null; + } catch (e) { + this.logger.warn('Unable to check recommendation', e); + return null; + } } /** From 0193b63070725a08f14741b88f255b13ae545886 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 19 Jul 2023 11:59:13 +0200 Subject: [PATCH 084/106] #RI-4601 - add list of commands for triggers and functions --- redisinsight/api/config/default.ts | 7 +++++++ redisinsight/api/scripts/default-commands.ts | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/redisinsight/api/config/default.ts b/redisinsight/api/config/default.ts index 1f863f4b54..6c3af8d7b1 100644 --- a/redisinsight/api/config/default.ts +++ b/redisinsight/api/config/default.ts @@ -195,6 +195,13 @@ export default { url: process.env.COMMANDS_REDISBLOOM_URL || 'https://raw.githubusercontent.com/RedisBloom/RedisBloom/master/commands.json', }, + { + name: 'triggers_and_functions', + url: process.env.COMMANDS_TRIGGERS_AND_FUNCTIONS_URL + || 'https://raw.githubusercontent.com/RedisGears/RedisGears/master/commands.json', + defaultUrl: process.env.COMMANDS_TRIGGERS_AND_FUNCTIONS_DEFAULT_URL + || 'https://s3.amazonaws.com/redisinsight.download/public/commands/triggers_and_functions.json', + }, ], connections: { timeout: parseInt(process.env.CONNECTIONS_TIMEOUT_DEFAULT, 10) || 30 * 1_000, // 30 sec diff --git a/redisinsight/api/scripts/default-commands.ts b/redisinsight/api/scripts/default-commands.ts index 46674239c7..4bc1403da5 100644 --- a/redisinsight/api/scripts/default-commands.ts +++ b/redisinsight/api/scripts/default-commands.ts @@ -8,10 +8,10 @@ const COMMANDS_CONFIG = get('commands'); async function init() { try { - await Promise.all(COMMANDS_CONFIG.map(async ({ name, url }) => { + await Promise.all(COMMANDS_CONFIG.map(async ({ name, url, defaultUrl }) => { try { console.log(`Trying to get ${name} commands...`); - const { data } = await axios.get(url, { + const { data } = await axios.get(defaultUrl || url, { responseType: 'text', transformResponse: [(raw) => raw], }); From 9d7475c97b825da921234b6976c0686e9e26ce49 Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Wed, 19 Jul 2023 15:25:08 +0200 Subject: [PATCH 085/106] #RI-4721 - [Prod][BE] Index creation error for redis 7.1+ --- .../modules/browser/services/redisearch/redisearch.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts index e8f3e71358..4274e77225 100644 --- a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts +++ b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts @@ -93,7 +93,7 @@ export class RedisearchService { ); } } catch (error) { - if (!error.message?.includes('Unknown Index name')) { + if (!error.message?.toLowerCase()?.includes('unknown index name')) { throw error; } } From 549f16e3660e4a6735e93d167f7f1d2508ade1e6 Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Thu, 20 Jul 2023 00:38:31 +0200 Subject: [PATCH 086/106] #RI-4768 - Change design for recommendations --- .../ui/src/assets/img/icons/snooze.svg | 10 +++ .../ui/src/assets/img/icons/stars.svg | 7 ++ .../recommendation/Recommendation.tsx | 65 ++++++++++----- .../recommendation/styles.module.scss | 83 ++++++++++++------- .../styles.module.scss | 2 +- 5 files changed, 113 insertions(+), 54 deletions(-) create mode 100644 redisinsight/ui/src/assets/img/icons/snooze.svg create mode 100644 redisinsight/ui/src/assets/img/icons/stars.svg diff --git a/redisinsight/ui/src/assets/img/icons/snooze.svg b/redisinsight/ui/src/assets/img/icons/snooze.svg new file mode 100644 index 0000000000..36dd164a6d --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/snooze.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/icons/stars.svg b/redisinsight/ui/src/assets/img/icons/stars.svg new file mode 100644 index 0000000000..21f4711314 --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/stars.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx index e91ac53084..5099cdc4bc 100644 --- a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx +++ b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx @@ -30,6 +30,8 @@ import { IRecommendationsStatic, IRecommendationParams } from 'uiSrc/slices/inte import _content from 'uiSrc/constants/dbAnalysisRecommendations.json' import RediStackDarkMin from 'uiSrc/assets/img/modules/redistack/RediStackDark-min.svg' import RediStackLightMin from 'uiSrc/assets/img/modules/redistack/RediStackLight-min.svg' +import { ReactComponent as SnoozeIcon } from 'uiSrc/assets/img/icons/snooze.svg' +import { ReactComponent as StarsIcon } from 'uiSrc/assets/img/icons/stars.svg' import styles from './styles.module.scss' @@ -118,9 +120,17 @@ const Recommendation = ({ ) } - const handleDelete = () => { + const handleDelete = (event: React.MouseEvent) => { + event.stopPropagation() + event.preventDefault() setIsLoading(true) - dispatch(deleteLiveRecommendations([id], onSuccessActionDelete, () => setIsLoading(false))) + dispatch( + deleteLiveRecommendations( + [id], + onSuccessActionDelete, + () => setIsLoading(false) + ) + ) } const onSuccessActionDelete = () => { @@ -149,6 +159,19 @@ const Recommendation = ({ const recommendationContent = () => ( + {!isUndefined(tutorial) && ( + + { tutorial ? 'Start Tutorial' : 'Workbench' } + + )} {renderRecommendationContent( recommendationsContent[name]?.content, params, @@ -168,26 +191,6 @@ const Recommendation = ({ )}
- - Snooze - - {!isUndefined(tutorial) && ( - - { tutorial ? 'Tutorial' : 'Workbench' } - - )}
) @@ -228,6 +231,24 @@ const Recommendation = ({ {title} + + + + + Date: Thu, 20 Jul 2023 01:05:37 +0200 Subject: [PATCH 087/106] #RI-4768 - fix tests --- .../components/recommendation/Recommendation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx index 5099cdc4bc..eca9c54057 100644 --- a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx +++ b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx @@ -245,7 +245,7 @@ const Recommendation = ({ className={styles.snoozeBtn} onClick={handleDelete} aria-label="snooze recommendation" - data-testid={`snooze-${name}-btn`} + data-testid={`${name}-delete-btn`} /> From 5c3b0d2d13c2f64aa58ba1b1c8b5cfd10d7c90de Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 20 Jul 2023 15:29:17 +0800 Subject: [PATCH 088/106] #RI-4679 - add hello 3 to unsupport commands and add custom error message --- .../cli/utils/getUnsupportedCommands.spec.ts | 2 +- .../cli/utils/getUnsupportedCommands.ts | 1 + .../utils/getUnsupportedCommands.spec.ts | 2 +- .../workbench/utils/getUnsupportedCommands.ts | 1 + ...databases-id-cli-uuid-send_command.test.ts | 11 ++++++++++ .../GET-info-cli-unsupported-commands.test.ts | 2 +- ...es-id-workbench-command_executions.test.ts | 9 ++++++++ .../components/cli-body/CliBodyWrapper.tsx | 9 +++++++- .../CommonErrorResponse.tsx | 7 +++++- redisinsight/ui/src/constants/cliOutput.tsx | 22 +++++++++++++++++++ redisinsight/ui/src/constants/commands.ts | 1 + 11 files changed, 62 insertions(+), 5 deletions(-) diff --git a/redisinsight/api/src/modules/cli/utils/getUnsupportedCommands.spec.ts b/redisinsight/api/src/modules/cli/utils/getUnsupportedCommands.spec.ts index 887f5963ca..3421c65662 100644 --- a/redisinsight/api/src/modules/cli/utils/getUnsupportedCommands.spec.ts +++ b/redisinsight/api/src/modules/cli/utils/getUnsupportedCommands.spec.ts @@ -2,7 +2,7 @@ import { getUnsupportedCommands } from './getUnsupportedCommands'; describe('cli unsupported commands', () => { it('should return correct list', () => { - const expectedResult = ['monitor', 'subscribe', 'psubscribe', 'ssubscribe', 'sync', 'psync', 'script debug']; + const expectedResult = ['monitor', 'subscribe', 'psubscribe', 'ssubscribe', 'sync', 'psync', 'script debug', 'hello 3']; expect(getUnsupportedCommands()).toEqual(expectedResult); }); diff --git a/redisinsight/api/src/modules/cli/utils/getUnsupportedCommands.ts b/redisinsight/api/src/modules/cli/utils/getUnsupportedCommands.ts index 119cf4da1a..8651aba357 100644 --- a/redisinsight/api/src/modules/cli/utils/getUnsupportedCommands.ts +++ b/redisinsight/api/src/modules/cli/utils/getUnsupportedCommands.ts @@ -10,6 +10,7 @@ export enum CliToolUnsupportedCommands { Sync = 'sync', PSync = 'psync', ScriptDebug = 'script debug', + Hello3 = 'hello 3', } export const getUnsupportedCommands = (): string[] => [ diff --git a/redisinsight/api/src/modules/workbench/utils/getUnsupportedCommands.spec.ts b/redisinsight/api/src/modules/workbench/utils/getUnsupportedCommands.spec.ts index 69c2a2d35e..c5c2f19d96 100644 --- a/redisinsight/api/src/modules/workbench/utils/getUnsupportedCommands.spec.ts +++ b/redisinsight/api/src/modules/workbench/utils/getUnsupportedCommands.spec.ts @@ -2,7 +2,7 @@ import { getUnsupportedCommands } from './getUnsupportedCommands'; describe('workbench unsupported commands', () => { it('should return correct list', () => { - const expectedResult = ['monitor', 'subscribe', 'psubscribe', 'ssubscribe', 'sync', 'psync', 'script debug', 'select']; + const expectedResult = ['monitor', 'subscribe', 'psubscribe', 'ssubscribe', 'sync', 'psync', 'script debug', 'select', 'hello 3']; expect(getUnsupportedCommands()).toEqual(expectedResult); }); diff --git a/redisinsight/api/src/modules/workbench/utils/getUnsupportedCommands.ts b/redisinsight/api/src/modules/workbench/utils/getUnsupportedCommands.ts index a2c9720ed2..6b45e2a2a4 100644 --- a/redisinsight/api/src/modules/workbench/utils/getUnsupportedCommands.ts +++ b/redisinsight/api/src/modules/workbench/utils/getUnsupportedCommands.ts @@ -11,6 +11,7 @@ export enum WorkbenchToolUnsupportedCommands { PSync = 'psync', ScriptDebug = 'script debug', Select = 'select', + Hello3 = 'hello 3', } export const getUnsupportedCommands = (): string[] => [ diff --git a/redisinsight/api/test/api/cli/POST-databases-id-cli-uuid-send_command.test.ts b/redisinsight/api/test/api/cli/POST-databases-id-cli-uuid-send_command.test.ts index c933d5cb56..b6dbc75b32 100644 --- a/redisinsight/api/test/api/cli/POST-databases-id-cli-uuid-send_command.test.ts +++ b/redisinsight/api/test/api/cli/POST-databases-id-cli-uuid-send_command.test.ts @@ -850,6 +850,17 @@ describe('POST /databases/:instanceId/cli/:uuid/send-command', () => { expect(body.response).to.include('command is not supported by the RedisInsight CLI'); } }, + { + name: 'Should return error if try to run unsupported command (hello 3)', + data: { + command: `hello 3`, + }, + responseSchema, + checkFn: ({ body }) => { + expect(body.status).to.eql('fail'); + expect(body.response).to.include('command is not supported by the RedisInsight CLI'); + } + }, ].map(mainCheckFn); }); describe('Blocking commands', () => { diff --git a/redisinsight/api/test/api/info/GET-info-cli-unsupported-commands.test.ts b/redisinsight/api/test/api/info/GET-info-cli-unsupported-commands.test.ts index 5e9baceb92..422e785359 100644 --- a/redisinsight/api/test/api/info/GET-info-cli-unsupported-commands.test.ts +++ b/redisinsight/api/test/api/info/GET-info-cli-unsupported-commands.test.ts @@ -27,7 +27,7 @@ describe('GET /info/cli-unsupported-commands', () => { name: 'Should return array with unsupported commands for CLI tool', statusCode: 200, responseSchema, - responseBody: ['monitor', 'subscribe', 'psubscribe', 'ssubscribe', 'sync', 'psync', 'script debug'], + responseBody: ['monitor', 'subscribe', 'psubscribe', 'ssubscribe', 'sync', 'psync', 'script debug', 'hello 3'], }, ].map(mainCheckFn); }); diff --git a/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts b/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts index fae394bcf6..d36965f2a6 100644 --- a/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts +++ b/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts @@ -920,6 +920,15 @@ describe('POST /databases/:instanceId/workbench/command-executions', () => { expect(body[0].executionTime).to.eql(undefined); }, }, + { + name: 'Should return error if try to run unsupported command (hello 3)', + data: { + commands: [`hello 3`], + }, + checkFn: ({ body }) => { + expect(body[0].executionTime).to.eql(undefined); + }, + }, { name: 'Should return error if try to run blocking command', data: { diff --git a/redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.tsx b/redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.tsx index bf36ba8d25..c062ec80a6 100644 --- a/redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.tsx +++ b/redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.tsx @@ -19,7 +19,7 @@ import { processUnsupportedCommand, processUnrepeatableNumber, } from 'uiSrc/slices/cli/cli-output' -import { CommandMonitor, CommandPSubscribe, CommandSubscribe, Pages } from 'uiSrc/constants' +import { CommandMonitor, CommandPSubscribe, CommandSubscribe, CommandHello3, Pages } from 'uiSrc/constants' import { getCommandRepeat, isRepeatCountCorrect } from 'uiSrc/utils' import { ConnectionType } from 'uiSrc/slices/interfaces' import { ClusterNodeRole } from 'uiSrc/slices/interfaces/cli' @@ -114,6 +114,13 @@ const CliBodyWrapper = () => { return } + // Flow if HELLO 3 command was executed + if (checkUnsupportedCommand([CommandHello3.toLowerCase()], commandLine)) { + dispatch(concatToOutput(cliTexts.HELLO3_COMMAND_CLI())) + resetCommand() + return + } + if (unsupportedCommand) { dispatch(processUnsupportedCommand(commandLine, unsupportedCommand, resetCommand)) return diff --git a/redisinsight/ui/src/components/query-card/QueryCardCommonResult/components/CommonErrorResponse/CommonErrorResponse.tsx b/redisinsight/ui/src/components/query-card/QueryCardCommonResult/components/CommonErrorResponse/CommonErrorResponse.tsx index 2260d2c7b9..9cb35610e1 100644 --- a/redisinsight/ui/src/components/query-card/QueryCardCommonResult/components/CommonErrorResponse/CommonErrorResponse.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCardCommonResult/components/CommonErrorResponse/CommonErrorResponse.tsx @@ -11,7 +11,7 @@ import { } from 'uiSrc/utils' import { ModuleNotLoaded } from 'uiSrc/components' import { cliTexts, SelectCommand } from 'uiSrc/constants/cliOutput' -import { CommandMonitor, CommandPSubscribe, CommandSubscribe, Pages } from 'uiSrc/constants' +import { CommandMonitor, CommandPSubscribe, CommandSubscribe, CommandHello3, Pages } from 'uiSrc/constants' import { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli' import { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' @@ -39,6 +39,11 @@ const CommonErrorResponse = (id: string, command = '', result?: any) => { return cliTexts.PSUBSCRIBE_COMMAND(Pages.pubSub(instanceId)) } + // Flow if HELLO 3 command was executed + if (checkUnsupportedCommand([CommandHello3.toLowerCase()], commandLine)) { + return cliTexts.HELLO3_COMMAND() + } + const unsupportedCommand = checkUnsupportedCommand(unsupportedCommands, commandLine) if (result === null) { diff --git a/redisinsight/ui/src/constants/cliOutput.tsx b/redisinsight/ui/src/constants/cliOutput.tsx index f67b8a9358..24385ed259 100644 --- a/redisinsight/ui/src/constants/cliOutput.tsx +++ b/redisinsight/ui/src/constants/cliOutput.tsx @@ -106,6 +106,28 @@ export const cliTexts = { '\n', ] ), + HELLO3_COMMAND: () => ( + + {'RedisInsight does not support '} + + RESP3 + + {' at the moment, but we are working on it.'} + + ), + HELLO3_COMMAND_CLI: () => ( + [ + cliTexts.HELLO3_COMMAND(), + '\n', + ] + ), CLI_ERROR_MESSAGE: (message: string) => ( [ '\n', diff --git a/redisinsight/ui/src/constants/commands.ts b/redisinsight/ui/src/constants/commands.ts index 7c902ddaa7..2bcaed98d1 100644 --- a/redisinsight/ui/src/constants/commands.ts +++ b/redisinsight/ui/src/constants/commands.ts @@ -90,6 +90,7 @@ export enum CommandPrefix { export const CommandMonitor = 'MONITOR' export const CommandPSubscribe = 'PSUBSCRIBE' export const CommandSubscribe = 'SUBSCRIBE' +export const CommandHello3 = 'HELLO 3' export enum CommandRediSearch { Search = 'FT.SEARCH', From ed783a63f8a9553ac66837f63867b8f1ef19267d Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Thu, 20 Jul 2023 11:50:27 +0300 Subject: [PATCH 089/106] Fe/feature/ri 4579 module not loaded (#2349) * #RI-4579 - add no libraries screen, no triggers and functions module screen --------- Co-authored-by: mariasergeenko Co-authored-by: mariasergeenko <129392901+mariasergeenko@users.noreply.github.com> --- .../api/src/constants/redis-modules.ts | 2 +- .../src/utils/redis-modules-summary.spec.ts | 4 +- .../api/src/utils/redis-modules-summary.ts | 4 +- .../module-not-loaded/ModuleNotLoaded.tsx | 2 +- .../module-not-loaded/styles.module.scss | 4 + .../navigation-menu/NavigationMenu.tsx | 4 +- .../OnboardingFeatures.spec.tsx | 2 +- .../OnboardingFeatures.tsx | 2 +- .../constants/dbAnalysisRecommendations.json | 26 ++-- .../ui/src/constants/featuresHighlighting.tsx | 2 +- .../ui/src/constants/workbenchResults.ts | 14 +- .../components/auto-refresh/AutoRefresh.tsx | 7 +- .../TriggeredFunctionsPage.tsx | 2 +- .../NoLibrariesScreen.spec.tsx | 111 +++++++++++++ .../NoLibrariesScreen/NoLibrariesScreen.tsx | 147 ++++++++++++++++++ .../components/NoLibrariesScreen/index.ts | 0 .../NoLibrariesScreen/styles.module.scss | 110 +++++++++++++ .../pages/Functions/FunctionsPage.spec.tsx | 36 ++++- .../pages/Functions/FunctionsPage.tsx | 70 +++++---- .../FunctionsList/FunctionsList.spec.tsx | 27 ++++ .../FunctionsList/FunctionsList.tsx | 16 +- .../pages/Libraries/LibrariesPage.spec.tsx | 61 +++++++- .../pages/Libraries/LibrariesPage.tsx | 123 +++++++++------ .../LibrariesList/LibrariesList.spec.tsx | 41 ++++- .../LibrariesList/LibrariesList.tsx | 26 +++- .../NoLibrariesScreen.spec.tsx | 23 --- .../NoLibrariesScreen/NoLibrariesScreen.tsx | 40 ----- .../NoLibrariesScreen/styles.module.scss | 30 ---- .../triggeredFunctions/styles.modules.scss | 16 ++ .../ui/src/pages/workbench/constants.ts | 1 + .../ui/src/slices/interfaces/instances.ts | 3 +- .../slices/interfaces/triggeredFunctions.ts | 1 + .../triggeredFunctions.spec.ts | 34 ++++ .../triggeredFunctions/triggeredFunctions.ts | 6 + redisinsight/ui/src/telemetry/interfaces.ts | 2 +- redisinsight/ui/src/telemetry/pageViews.ts | 2 +- .../ui/src/telemetry/telemetryUtils.spec.ts | 4 +- .../ui/src/telemetry/telemetryUtils.ts | 4 +- redisinsight/ui/src/utils/cliHelper.tsx | 3 + .../triggers-and-functions-functions-page.ts | 1 + .../triggers-and-functions/libraries.e2e.ts | 1 + .../regression/browser/onboarding.e2e.ts | 2 +- 42 files changed, 792 insertions(+), 224 deletions(-) create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.tsx rename redisinsight/ui/src/pages/triggeredFunctions/{pages/Libraries => }/components/NoLibrariesScreen/index.ts (100%) create mode 100644 redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/styles.module.scss delete mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx delete mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.tsx delete mode 100644 redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss diff --git a/redisinsight/api/src/constants/redis-modules.ts b/redisinsight/api/src/constants/redis-modules.ts index c16a809a2b..3fa6644396 100644 --- a/redisinsight/api/src/constants/redis-modules.ts +++ b/redisinsight/api/src/constants/redis-modules.ts @@ -6,7 +6,7 @@ export enum AdditionalRedisModuleName { RedisJSON = 'ReJSON', RediSearch = 'search', RedisTimeSeries = 'timeseries', - 'Triggers & Functions' = 'redisgears' + 'Triggers and Functions' = 'redisgears' } export enum AdditionalSearchModuleName { diff --git a/redisinsight/api/src/utils/redis-modules-summary.spec.ts b/redisinsight/api/src/utils/redis-modules-summary.spec.ts index 966352972b..1e61ebeea2 100644 --- a/redisinsight/api/src/utils/redis-modules-summary.spec.ts +++ b/redisinsight/api/src/utils/redis-modules-summary.spec.ts @@ -9,7 +9,7 @@ const DEFAULT_SUMMARY = Object.freeze( RedisBloom: { loaded: false }, RedisJSON: { loaded: false }, RedisTimeSeries: { loaded: false }, - 'Triggers & Functions': { loaded: false }, + 'Triggers and Functions': { loaded: false }, customModules: [], }, ); @@ -54,7 +54,7 @@ const getRedisModulesSummaryTests = [ RedisJSON: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RediSearch: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RedisTimeSeries: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, - 'Triggers & Functions': { loaded: true, version: 10000, semanticVersion: '1.0.0' }, + 'Triggers and Functions': { loaded: true, version: 10000, semanticVersion: '1.0.0' }, customModules: [], }, }, diff --git a/redisinsight/api/src/utils/redis-modules-summary.ts b/redisinsight/api/src/utils/redis-modules-summary.ts index fd56f4ccf9..e9cbbfb7c7 100644 --- a/redisinsight/api/src/utils/redis-modules-summary.ts +++ b/redisinsight/api/src/utils/redis-modules-summary.ts @@ -24,7 +24,7 @@ export const DEFAULT_SUMMARY: IRedisModulesSummary = Object.freeze( RedisBloom: { loaded: false }, RedisJSON: { loaded: false }, RedisTimeSeries: { loaded: false }, - 'Triggers & Functions': { loaded: false }, + 'Triggers and Functions': { loaded: false }, customModules: [], }, ); @@ -69,7 +69,7 @@ export const getRedisModulesSummary = (modules: AdditionalRedisModule[] = []): I if (isTriggeredAndFunctionsAvailable([module])) { const triggeredAndFunctionsName = getEnumKeyBValue( AdditionalRedisModuleName, - AdditionalRedisModuleName['Triggers & Functions'], + AdditionalRedisModuleName['Triggers and Functions'], ); summary[triggeredAndFunctionsName] = getModuleSummaryToSent(module); return; diff --git a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx index 7bc14cf3a2..873791c73f 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx @@ -28,7 +28,7 @@ const MAX_ELEMENT_WIDTH = 1440 const renderTitle = (width: number, moduleName?: string) => (

- {`Looks like ${moduleName} is not available `} + {`${moduleName} ${moduleName === MODULE_TEXT_VIEW.redisgears ? 'are' : 'is'} not available `} {width > MAX_ELEMENT_WIDTH &&
} for this database

diff --git a/redisinsight/ui/src/components/messages/module-not-loaded/styles.module.scss b/redisinsight/ui/src/components/messages/module-not-loaded/styles.module.scss index 74a51df427..23899a2c4a 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/styles.module.scss +++ b/redisinsight/ui/src/components/messages/module-not-loaded/styles.module.scss @@ -7,6 +7,10 @@ font-weight: 600; word-break: break-word; margin-bottom: 20px; + + &::first-letter { + text-transform: uppercase; + } } .linksWrapper .text, diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx index 0eca6685ea..7a9f35cdc1 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx @@ -168,9 +168,9 @@ const NavigationMenu = () => { onboard: ONBOARDING_FEATURES.PUB_SUB_PAGE }, { - tooltipText: 'Triggers & Functions', + tooltipText: 'Triggers and Functions', pageName: PageNames.triggeredFunctions, - ariaLabel: 'Triggers & Functions', + ariaLabel: 'Triggers and Functions', onClick: () => handleGoPage(Pages.triggeredFunctions(connectedInstanceId)), dataTestId: 'triggered-functions-page-btn', connectedInstanceId, diff --git a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx index 9457d71d27..77edbca795 100644 --- a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx +++ b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx @@ -795,7 +795,7 @@ describe('ONBOARDING_FEATURES', () => { expect( render() ).toBeTruthy() - expect(screen.getByTestId('step-content')).toHaveTextContent('Triggers and functions can execute server-side functions triggered by certain events or data') + expect(screen.getByTestId('step-content')).toHaveTextContent('Triggers and Functions can execute server-side functions triggered by certain events or data operations to decrease latency and react in real time to database events.See the list of uploaded libraries, upload or delete libraries, or investigate and debug functions.') }) it('should call proper telemetry events', () => { diff --git a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx index 128370032d..d0d29a9478 100644 --- a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx +++ b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx @@ -469,7 +469,7 @@ const ONBOARDING_FEATURES = { }, TRIGGERED_FUNCTIONS_PAGE: { step: OnboardingSteps.TriggeredFunctionsPage, - title: 'Triggers & Functions', + title: 'Triggers and Functions', Inner: () => { const { id: connectedInstanceId = '' } = useSelector(connectedInstanceSelector) const history = useHistory() diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 9e04c58fc0..6a4133f6e5 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -1439,12 +1439,12 @@ }, "luaToFunctions": { "id": "luaToFunctions", - "title": "Consider using Triggers and Functions", + "title": "Consider using triggers and functions", "tutorial": "/quick-guides/triggers-and-functions/introduction.md", "content": [ { "type": "paragraph", - "value": "If you are using LUA scripts to run application logic inside Redis, consider using Triggers and Functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." + "value": "If you are using LUA scripts to run application logic inside Redis, consider using triggers and functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." }, { "type": "spacer", @@ -1452,7 +1452,7 @@ }, { "type": "paragraph", - "value": "Triggers and Functions can execute business logic on changes within a database, and read across all shards in clustered databases." + "value": "Triggers and functions can execute business logic on changes within a database, and read across all shards in clustered databases." }, { "type": "spacer", @@ -1500,12 +1500,12 @@ }, "functionsWithStreams": { "id": "functionsWithStreams", - "title": "Consider using Triggers and Functions to react in real-time to stream entries", + "title": "Consider using triggers and functions to react in real-time to stream entries", "tutorial": "/quick-guides/triggers-and-functions/introduction.md", "content": [ { "type": "paragraph", - "value": "If you need to manipulate your data based on Redis stream entries, consider using stream triggers that are a part of Triggers and Functions. It can help lower latency by moving business logic closer to the data." + "value": "If you need to manipulate your data based on Redis stream entries, consider using stream triggers that are a part of triggers and functions. It can help lower latency by moving business logic closer to the data." }, { "type": "spacer", @@ -1513,7 +1513,7 @@ }, { "type": "paragraph", - "value": "Try Triggers and Functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." + "value": "Try triggers and functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." }, { "type": "spacer", @@ -1529,7 +1529,7 @@ }, { "type": "span", - "value": "Triggers and Functions are part of " + "value": "Triggers and functions are part of " }, { "type": "link", @@ -1570,19 +1570,19 @@ }, { "type": "paragraph", - "value": "Try the interactive tutorial to learn more about Triggers and Functions." + "value": "Try the interactive tutorial to learn more about triggers and functions." } ], "badges": ["code_changes"] }, "functionsWithKeyspace": { "id": "functionsWithKeyspace", - "title": "Consider using Triggers and Functions to react in real-time to database changes", + "title": "Consider using triggers and functions to react in real-time to database changes", "tutorial": "/quick-guides/triggers-and-functions/introduction.md", "content": [ { "type": "paragraph", - "value": "If you need to manipulate your data based on keyspace notifications, consider using keyspace triggers that are a part of Triggers and Functions. It can help lower latency by moving business logic closer to the data." + "value": "If you need to manipulate your data based on keyspace notifications, consider using keyspace triggers that are a part of triggers and functions. It can help lower latency by moving business logic closer to the data." }, { "type": "spacer", @@ -1590,7 +1590,7 @@ }, { "type": "paragraph", - "value": "Try Triggers and Functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." + "value": "Try triggers and functions to take advantage of Javascript's vast ecosystem of libraries and frameworks and modern, expressive syntax." }, { "type": "spacer", @@ -1606,7 +1606,7 @@ }, { "type": "span", - "value": "Triggers and Functions are part of " + "value": "Triggers and functions are part of " }, { "type": "link", @@ -1647,7 +1647,7 @@ }, { "type": "paragraph", - "value": "Try the interactive tutorial to learn more about Triggers and Functions." + "value": "Try the interactive tutorial to learn more about triggers and functions." } ], "badges": ["code_changes"] diff --git a/redisinsight/ui/src/constants/featuresHighlighting.tsx b/redisinsight/ui/src/constants/featuresHighlighting.tsx index d91fe5de49..7bb5f80529 100644 --- a/redisinsight/ui/src/constants/featuresHighlighting.tsx +++ b/redisinsight/ui/src/constants/featuresHighlighting.tsx @@ -14,7 +14,7 @@ interface BuildHighlightingFeature { export const BUILD_FEATURES: Record = { [PageNames.triggeredFunctions]: { type: 'tooltip', - title: 'Triggers & Functions', + title: 'Triggers and Functions', content: 'Triggers and Functions can execute server-side functions triggered by events or data operations to decrease latency and react in real time to database events.', page: PageNames.triggeredFunctions, asPageFeature: true diff --git a/redisinsight/ui/src/constants/workbenchResults.ts b/redisinsight/ui/src/constants/workbenchResults.ts index 8a1314e799..4da6b35df4 100644 --- a/redisinsight/ui/src/constants/workbenchResults.ts +++ b/redisinsight/ui/src/constants/workbenchResults.ts @@ -45,7 +45,18 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = ], additionalText: ['With this capability you can query streaming data without needing to store all the elements of the stream.'], link: 'https://redis.io/docs/stack/bloom/' - } + }, + [RedisDefaultModules.RedisGears]: { + text: ['Triggers and functions add the capability to execute server-side functions that are triggered by events or data operations to:'], + improvements: [ + 'Speed up applications by running the application logic where the data lives', + 'Eliminate the need to maintain the same code across different applications by moving application functionality inside the Redis database', + 'Maintain consistent data when applications react to changing real-time conditions in the keyspace instead of using Pub/Sub notifications', + 'Improve code resiliency by backing up and replicating triggers and functions along with the database' + ], + additionalText: ['Triggers and functions work with a JavaScript engine, which lets you take advantage of JavaScript’s vast ecosystem of libraries and frameworks and modern, expressive syntax.'], + link: 'https://redis.io/docs/interact/programmability/functions-intro' + }, } export const MODULE_TEXT_VIEW: { [key in RedisDefaultModules]?: string } = { @@ -53,4 +64,5 @@ export const MODULE_TEXT_VIEW: { [key in RedisDefaultModules]?: string } = { [RedisDefaultModules.ReJSON]: 'RedisJSON', [RedisDefaultModules.Search]: 'RediSearch', [RedisDefaultModules.TimeSeries]: 'RedisTimeSeries', + [RedisDefaultModules.RedisGears]: 'triggers and functions', } diff --git a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx b/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx index 8ef07d48b9..d5bf37c87a 100644 --- a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx +++ b/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx @@ -35,6 +35,7 @@ export interface Props { onEnableAutoRefresh?: (enableAutoRefresh: boolean, refreshRate: string) => void onChangeAutoRefreshRate?: (enableAutoRefresh: boolean, refreshRate: string) => void iconSize?: EuiButtonIconSizes + disabled?: boolean } const TIMEOUT_TO_UPDATE_REFRESH_TIME = 1_000 * MINUTE // once a minute @@ -51,7 +52,8 @@ const AutoRefresh = ({ onRefreshClicked, onEnableAutoRefresh, onChangeAutoRefreshRate, - iconSize = 'm' + iconSize = 'm', + disabled, }: Props) => { let intervalText: NodeJS.Timeout let intervalRefresh: NodeJS.Timeout @@ -180,7 +182,7 @@ const AutoRefresh = ({ { const dispatch = useDispatch() const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}` - setTitle(`${dbName} - Triggers & Functions`) + setTitle(`${dbName} - Triggers and Functions`) useEffect(() => () => { dispatch(setLastTriggeredFunctionsPage(pathnameRef.current)) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx new file mode 100644 index 0000000000..0b29b8a9d6 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx @@ -0,0 +1,111 @@ +import React from 'react' +import reactRouterDom from 'react-router-dom' +import { cloneDeep } from 'lodash' +import { instance, mock } from 'ts-mockito' +import { cleanup, clearStoreActions, render, fireEvent, screen, mockedStore } from 'uiSrc/utils/test-utils' +import { resetWorkbenchEASearch, setWorkbenchEAMinimized } from 'uiSrc/slices/app/context' +import { workbenchGuidesSelector } from 'uiSrc/slices/workbench/wb-guides' + +import NoLibrariesScreen, { IProps } from './NoLibrariesScreen' + +const mockedProps = mock() + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +jest.mock('uiSrc/slices/workbench/wb-guides', () => ({ + ...jest.requireActual('uiSrc/slices/workbench/wb-guides'), + workbenchGuidesSelector: jest.fn().mockReturnValue({ + items: [{ + label: 'Quick guides', + type: 'group', + children: [ + { + label: 'Quick guides', + type: 'group', + children: [ + { + type: 'internal-link', + id: 'document-capabilities', + label: 'Triggers and Functions', + args: { + path: '/quick-guides/triggers-and-functions/introduction.md', + }, + }, + ] + } + ] + }], + }), +})) + +describe('NoLibrariesScreen', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call proper actions and push to quick guides page ', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + fireEvent.click(screen.getByTestId('no-libraries-tutorial-link')) + + const expectedActions = [setWorkbenchEAMinimized(false), resetWorkbenchEASearch()] + expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) + expect(pushMock).toBeCalledWith('/instanceId/workbench?path=quick-guides/0/0/0') + }) + + it('should call proper actions and push to workbench page', () => { + (workbenchGuidesSelector as jest.Mock).mockImplementation(() => ({ + items: [] + })) + + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + fireEvent.click(screen.getByTestId('no-libraries-tutorial-link')) + + const expectedActions = [setWorkbenchEAMinimized(false), resetWorkbenchEASearch()] + expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) + expect(pushMock).toBeCalledWith('/instanceId/workbench') + }) + + it('should have proper text when module is loaded', () => { + render() + + expect(screen.getByTestId('no-libraries-title')).toHaveTextContent('Triggers and functions') + expect(screen.getByTestId('no-libraries-action-text')).toHaveTextContent('Upload a new library to start working with triggers and functions or try the interactive tutorial to learn more.') + }) + + it('should have proper text when module is not loaded', () => { + render() + + expect(screen.getByTestId('no-libraries-title')).toHaveTextContent('triggers and functions are not available for this database') + expect(screen.getByTestId('no-libraries-action-text')).toHaveTextContent('Create a free Redis Stack database which extends the core capabilities of open-source Redis and try the interactive tutorial to learn how to work with triggers and functions.') + }) + + it('should call proper actions and push to workbench page', () => { + (workbenchGuidesSelector as jest.Mock).mockImplementation(() => ({ + items: [] + })) + + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + fireEvent.click(screen.getByTestId('no-libraries-tutorial-link')) + + const expectedActions = [setWorkbenchEAMinimized(false), resetWorkbenchEASearch()] + expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) + expect(pushMock).toBeCalledWith('/instanceId/workbench') + }) +}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.tsx b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.tsx new file mode 100644 index 0000000000..982c6686fb --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.tsx @@ -0,0 +1,147 @@ +import React from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { useParams, useHistory } from 'react-router-dom' +import cx from 'classnames' +import { + EuiTextColor, + EuiText, + EuiTitle, + EuiButton, + EuiButtonEmpty, + EuiLink, + EuiIcon, +} from '@elastic/eui' + +import { workbenchGuidesSelector } from 'uiSrc/slices/workbench/wb-guides' +import { resetWorkbenchEASearch, setWorkbenchEAMinimized } from 'uiSrc/slices/app/context' +import { EAManifestFirstKey, Pages, MODULE_NOT_LOADED_CONTENT as CONTENT, MODULE_TEXT_VIEW } from 'uiSrc/constants' +import { ReactComponent as CheerIcon } from 'uiSrc/assets/img/icons/cheer.svg' +import TriggersAndFunctionsImage from 'uiSrc/assets/img/onboarding-emoji.svg' +import { RedisDefaultModules } from 'uiSrc/slices/interfaces' +import { findMarkdownPathByPath } from 'uiSrc/utils' + +import styles from './styles.module.scss' + +export interface IProps { + isModuleLoaded: boolean + isAddLibraryPanelOpen?: boolean + onAddLibrary?: () => void +} + +const ListItem = ({ item }: { item: string }) => ( +
  • +
    + +
    + {item} +
  • +) + +const moduleName = MODULE_TEXT_VIEW[RedisDefaultModules.RedisGears] + +const NoLibrariesScreen = (props: IProps) => { + const { isAddLibraryPanelOpen, isModuleLoaded, onAddLibrary = () => {} } = props + const { items: guides } = useSelector(workbenchGuidesSelector) + + const { instanceId = '' } = useParams<{ instanceId: string }>() + const dispatch = useDispatch() + const history = useHistory() + + const goToTutorial = () => { + // triggers and functions tutorial does not upload + dispatch(setWorkbenchEAMinimized(false)) + const quickGuidesPath = findMarkdownPathByPath(guides, '/quick-guides/triggers-and-functions/introduction.md') + if (quickGuidesPath) { + history.push(`${Pages.workbench(instanceId)}?path=${EAManifestFirstKey.GUIDES}/${quickGuidesPath}`) + } + + dispatch(resetWorkbenchEASearch()) + history.push(Pages.workbench(instanceId)) + } + + return ( +
    +
    +
    + +

    + {isModuleLoaded + ? 'Triggers and functions' + : `${moduleName} are not available for this database`} +

    +
    + + {CONTENT[RedisDefaultModules.RedisGears]?.text.map((item: string) => item)} + +
      + {CONTENT[RedisDefaultModules.RedisGears]?.improvements.map((item: string) => ( + + ))} +
    + {CONTENT[RedisDefaultModules.RedisGears]?.additionalText.map((item: string, idx: number) => ( + + {item} + + ))} + + {isModuleLoaded + ? 'Upload a new library to start working with triggers and functions or try the interactive tutorial to learn more.' + : 'Create a free Redis Stack database which extends the core capabilities of open-source Redis and try the interactive tutorial to learn how to work with triggers and functions.'} + +
    +
    + + Tutorial + + {isModuleLoaded + ? ( + + + Library + + ) + : ( + + + Get Started For Free + + + )} +
    +
    + {!isAddLibraryPanelOpen && ( +
    + +
    + )} +
    + ) +} + +export default NoLibrariesScreen diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/index.ts b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/index.ts similarity index 100% rename from redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/index.ts rename to redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/index.ts diff --git a/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/styles.module.scss new file mode 100644 index 0000000000..d0ab24b3c6 --- /dev/null +++ b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/styles.module.scss @@ -0,0 +1,110 @@ +.wrapper { + padding: 32px 30px; + display: flex; + + .content { + width: 50%; + padding-right: 48px; + + &.fullWidth { + padding-right: 0; + width: 100%; + } + } + + .imageWrapper { + padding-left: 48px; + width: 50%; + + .image { + width: 100%; + } + } + + .contentWrapper { + text-align: left; + } + + .title { + font-family: 'Graphik', sans-serif; + font-size: 28px; + font-weight: 500; + word-break: break-word; + margin-bottom: 16px; + + &::first-letter { + text-transform: uppercase; + } + } + + .linksWrapper .text, + .text { + font-family: 'Graphik', sans-serif; + font-size: 14px; + line-height: 17px; + word-break: break-word; + color: var(--wbTextColor) !important; + text-decoration: none !important; + } + + .additionalText { + font-family: 'Graphik', sans-serif; + font-size: 14px; + line-height: 21px; + word-break: break-word; + color: var(--textColorShade) !important; + } + + .bigText { + font-family: 'Graphik', sans-serif; + font-size: 18px; + line-height: 27px; + word-break: break-word; + color: var(--textColorShade) !important; + margin-bottom: 16px; + } + + .listItem { + display: flex; + margin-bottom: 12px; + + .iconWrapper { + display: flex; + flex: 0 0 16px; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + background: var(--wbActiveIconColor); + border-radius: 50%; + margin-right: 10px; + } + + .listIcon { + width: 10px; + height: 10px; + + path { + fill: var(--euiPageBackgroundColor); + } + } + } + + .linksWrapper { + display: flex; + justify-content: flex-end; + align-items: center; + + .link { + margin-right: 16px; + } + } + + .btn :global(.euiButton__text) { + color: var(--euiColorPrimaryText) !important; + } + + .marginBottom { + margin-bottom: 16px; + } +} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx index 85de5307db..873e3b70b0 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx @@ -1,12 +1,15 @@ import React from 'react' import { cloneDeep } from 'lodash' +import reactRouterDom from 'react-router-dom' import { cleanup, mockedStore, render, screen, fireEvent, act } from 'uiSrc/utils/test-utils' import { getTriggeredFunctionsFunctionsList, triggeredFunctionsFunctionsSelector, } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { TRIGGERED_FUNCTIONS_FUNCTIONS_LIST_MOCKED_DATA } from 'uiSrc/mocks/data/triggeredFunctions' +import { Pages } from 'uiSrc/constants' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import FunctionsPage from './FunctionsPage' @@ -23,6 +26,14 @@ jest.mock('uiSrc/slices/triggeredFunctions/triggeredFunctions', () => ({ }), })) +jest.mock('uiSrc/slices/instances/instances', () => ({ + ...jest.requireActual('uiSrc/slices/instances/instances'), + connectedInstanceSelector: jest.fn().mockReturnValue({ + id: 'instanceId', + modules: [{ name: 'redisgears' }] + }), +})) + let store: typeof mockedStore beforeEach(() => { cleanup() @@ -38,16 +49,21 @@ describe('FunctionsPage', () => { }) it('should fetch list of functions', () => { - (triggeredFunctionsFunctionsSelector as jest.Mock).mockReturnValueOnce({ - data: null, - loading: false - }) render() const expectedActions = [getTriggeredFunctionsFunctionsList()] expect(store.getActions()).toEqual(expectedActions) }) + it('should not fetch list of functions if there are no triggers and functions module', () => { + (connectedInstanceSelector as jest.Mock).mockReturnValueOnce({ + modules: [{ name: 'custom' }], + }) + render() + + expect(store.getActions()).toEqual([]) + }) + it('should render libraries list', async () => { (triggeredFunctionsFunctionsSelector as jest.Mock).mockReturnValueOnce({ data: mockedFunctions, @@ -133,4 +149,16 @@ describe('FunctionsPage', () => { } }) }) + + it('should history push and call proper actions', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + fireEvent.click(screen.getByTestId('no-libraries-add-library-btn')) + + expect(pushMock) + .toBeCalledWith(Pages.triggeredFunctionsLibraries('instanceId')) + }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx index 1135a21286..54bbd7c002 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx @@ -1,19 +1,21 @@ import React, { useEffect, useState } from 'react' import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiResizableContainer, } from '@elastic/eui' - import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' +import { useHistory } from 'react-router-dom' import cx from 'classnames' import { find, pick } from 'lodash' import { fetchTriggeredFunctionsFunctionsList, setSelectedFunctionToShow, triggeredFunctionsFunctionsSelector, + setAddLibraryFormOpen, } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { Pages } from 'uiSrc/constants' +import { isTriggeredAndFunctionsAvailable, Nullable } from 'uiSrc/utils' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { TriggeredFunctionsFunction } from 'uiSrc/slices/interfaces/triggeredFunctions' -import { Nullable } from 'uiSrc/utils' - import { LIST_OF_FUNCTION_NAMES } from 'uiSrc/pages/triggeredFunctions/constants' +import NoLibrariesScreen from 'uiSrc/pages/triggeredFunctions/components/NoLibrariesScreen' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { getFunctionsLengthByType } from 'uiSrc/utils/triggered-functions/utils' import FunctionsList from './components/FunctionsList' @@ -23,19 +25,23 @@ import styles from './styles.module.scss' export const firstPanelId = 'functions-left-panel' export const secondPanelId = 'functions-right-panel' +const NoFunctionsMessage: React.ReactNode = (No Functions found) const FunctionsPage = () => { const { lastRefresh, loading, data: functions, selected } = useSelector(triggeredFunctionsFunctionsSelector) + const { modules, id: instanceId } = useSelector(connectedInstanceSelector) const [items, setItems] = useState([]) const [filterValue, setFilterValue] = useState('') const [selectedRow, setSelectedRow] = useState>(null) - const { instanceId } = useParams<{ instanceId: string }>() const dispatch = useDispatch() + const history = useHistory() useEffect(() => { - updateList() - }, []) + if (isModuleLoaded) { + updateList() + } + }, [modules]) useEffect(() => { applyFiltering() @@ -97,6 +103,17 @@ const FunctionsPage = () => { setItems(itemsTemp || []) } + const onAddLibrary = () => { + dispatch(setAddLibraryFormOpen(true)) + history.push(Pages.triggeredFunctionsLibraries(instanceId)) + } + + const isModuleLoaded = isTriggeredAndFunctionsAvailable(modules) + + const message = functions?.length + ? NoFunctionsMessage + : () + return ( { className="triggeredFunctions__topPanel" > - {!!functions?.length && ( - - )} + @@ -148,16 +164,16 @@ const FunctionsPage = () => {
    )} - {functions && ( - - )} +
    { } }) }) + + it('should render disabled auto refresh btn', () => { + render() + + expect(screen.getByTestId('refresh-functions-btn')).toBeDisabled() + }) + + it('should call proper telemetry events when sorting is changed', () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + const { container } = render() + + fireEvent.click(container.querySelector('[data-test-subj="tableHeaderSortButton"') as HTMLInputElement) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_FUNCTIONS_SORTED, + eventData: { + databaseId: 'instanceId', + direction: 'asc', + field: 'name', + } + }) + + sendEventTelemetry.mockRestore() + }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx index 690386f0c0..5ed3a800a0 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/components/FunctionsList/FunctionsList.tsx @@ -18,12 +18,12 @@ export interface Props { lastRefresh: Nullable selectedRow: Nullable onSelectRow: (item: TriggeredFunctionsFunction) => void + message: React.ReactNode + isRefreshDisabled: boolean } -const NoFunctionsMessage: React.ReactNode = (No Functions found) - const FunctionsList = (props: Props) => { - const { items, loading, onRefresh, lastRefresh, selectedRow, onSelectRow } = props + const { items, loading, onRefresh, lastRefresh, selectedRow, onSelectRow, message, isRefreshDisabled } = props const [sort, setSort] = useState>(undefined) const { instanceId } = useParams<{ instanceId: string }>() @@ -112,6 +112,7 @@ const FunctionsList = (props: Props) => { onRefresh={() => onRefresh?.()} onRefreshClicked={handleRefreshClicked} onEnableAutoRefresh={handleEnableAutoRefresh} + disabled={isRefreshDisabled} testid="refresh-functions-btn" />
    @@ -126,9 +127,14 @@ const FunctionsList = (props: Props) => { className: isRowSelected(row, selectedRow) ? 'selected' : '', 'data-testid': `row-${row.name}`, })} - message={NoFunctionsMessage} + message={message} onTableChange={handleSorting} - className={cx('inMemoryTableDefault', 'noBorders', 'triggeredFunctions__table')} + className={cx( + 'inMemoryTableDefault', + 'noBorders', + 'triggeredFunctions__table', + { triggeredFunctions__emptyTable: !items?.length } + )} data-testid="functions-list-table" />
    diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx index 94fa27986a..15d81a72c5 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.spec.tsx @@ -5,7 +5,10 @@ import { cleanup, mockedStore, render, screen, fireEvent, act } from 'uiSrc/util import { getTriggeredFunctionsLibrariesList, triggeredFunctionsLibrariesSelector, + triggeredFunctionsAddLibrarySelector, + setAddLibraryFormOpen, } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { TRIGGERED_FUNCTIONS_LIBRARIES_LIST_MOCKED_DATA } from 'uiSrc/mocks/data/triggeredFunctions' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' @@ -17,6 +20,9 @@ jest.mock('uiSrc/slices/triggeredFunctions/triggeredFunctions', () => ({ loading: false, data: null }), + triggeredFunctionsAddLibrarySelector: jest.fn().mockReturnValue({ + open: false, + }), })) jest.mock('uiSrc/telemetry', () => ({ @@ -24,6 +30,14 @@ jest.mock('uiSrc/telemetry', () => ({ sendEventTelemetry: jest.fn(), })) +jest.mock('uiSrc/slices/instances/instances', () => ({ + ...jest.requireActual('uiSrc/slices/instances/instances'), + connectedInstanceSelector: jest.fn().mockReturnValue({ + id: 'instanceId', + modules: [{ name: 'redisgears' }] + }), +})) + let store: typeof mockedStore beforeEach(() => { cleanup() @@ -45,6 +59,16 @@ describe('LibrariesPage', () => { expect(store.getActions()).toEqual(expectedActions) }) + it('should not fetch list of libraries if there are no triggers and functions module', () => { + (connectedInstanceSelector as jest.Mock).mockReturnValueOnce({ + modules: [{ name: 'custom' }], + }) + + render() + + expect(store.getActions()).toEqual([]) + }) + it('should render message when no libraries uploaded', () => { (triggeredFunctionsLibrariesSelector as jest.Mock).mockReturnValueOnce({ data: [], @@ -52,7 +76,7 @@ describe('LibrariesPage', () => { }) render() - expect(screen.getByTestId('triggered-functions-welcome')).toBeInTheDocument() + expect(screen.getByTestId('no-libraries-component')).toBeInTheDocument() }) it('should render libraries list', () => { @@ -60,9 +84,9 @@ describe('LibrariesPage', () => { data: mockedLibraries, loading: false }) + render() - expect(screen.queryByTestId('triggered-functions-welcome')).not.toBeInTheDocument() expect(screen.getByTestId('libraries-list-table')).toBeInTheDocument() expect(screen.queryAllByTestId(/^row-/).length).toEqual(3) }) @@ -135,7 +159,6 @@ describe('LibrariesPage', () => { fireEvent.click(screen.getByTestId('btn-add-library')) - expect(screen.getByTestId('lib-add-form')).toBeInTheDocument() expect(sendEventTelemetry).toBeCalledWith({ event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LOAD_LIBRARY_CLICKED, eventData: { @@ -144,16 +167,38 @@ describe('LibrariesPage', () => { }) }) - it('should close library add form and sent proper telemetry event', () => { - render() + it('should open details', async () => { + (triggeredFunctionsAddLibrarySelector as jest.Mock).mockReturnValueOnce({ + open: true, + }) - fireEvent.click(screen.getByTestId('btn-add-library')) + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render() expect(screen.getByTestId('lib-add-form')).toBeInTheDocument() - fireEvent.click(screen.getByTestId('close-add-form-btn')) + fireEvent.change(screen.getByTestId('code-value'), { target: { value: 'code' } }) + + await act(() => { + fireEvent.click(screen.getByTestId('add-library-btn-submit')) + }) - expect(screen.queryByTestId('lib-add-form')).not.toBeInTheDocument() + // Library is default name when can not parse real name from code + expect(screen.getByTestId('lib-details-Library')).toBeInTheDocument() + }) + + it('should sent proper telemetry event on close add library form', () => { + (triggeredFunctionsAddLibrarySelector as jest.Mock).mockReturnValueOnce({ + open: true, + loading: false + }) + + render() + + fireEvent.click(screen.getByTestId('close-add-form-btn')) expect(sendEventTelemetry).toBeCalledWith({ event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LOAD_LIBRARY_CANCELLED, diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx index 16610398af..5a53914b91 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx @@ -6,22 +6,25 @@ import { EuiFlexItem, EuiLoadingSpinner, EuiResizableContainer, + EuiToolTip, } from '@elastic/eui' import { isNull, find } from 'lodash' import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' import cx from 'classnames' import { fetchTriggeredFunctionsLibrariesList, setSelectedLibraryToShow, setTriggeredFunctionsSelectedLibrary, triggeredFunctionsLibrariesSelector, + setAddLibraryFormOpen, + triggeredFunctionsAddLibrarySelector, } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { isTriggeredAndFunctionsAvailable, Nullable } from 'uiSrc/utils' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { TriggeredFunctionsLibrary } from 'uiSrc/slices/interfaces/triggeredFunctions' -import { Nullable } from 'uiSrc/utils' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import NoLibrariesScreen from './components/NoLibrariesScreen' +import NoLibrariesScreen from 'uiSrc/pages/triggeredFunctions/components/NoLibrariesScreen' import LibrariesList from './components/LibrariesList' import LibraryDetails from './components/LibraryDetails' import AddLibrary from './components/AddLibrary' @@ -30,25 +33,37 @@ import styles from './styles.module.scss' export const firstPanelId = 'libraries-left-panel' export const secondPanelId = 'libraries-right-panel' +const NoLibrariesMessage: React.ReactNode = (No Libraries found) const LibrariesPage = () => { - const { lastRefresh, loading, data: libraries, selected } = useSelector(triggeredFunctionsLibrariesSelector) + const { + lastRefresh, loading, data: libraries, selected, + } = useSelector(triggeredFunctionsLibrariesSelector) + const { open: isAddLibraryPanelOpen } = useSelector(triggeredFunctionsAddLibrarySelector) + const { modules, id: instanceId } = useSelector(connectedInstanceSelector) const [items, setItems] = useState([]) const [filterValue, setFilterValue] = useState('') const [selectedRow, setSelectedRow] = useState>(null) - const [isAddLibraryPanelOpen, setIsAddLibraryPanelOpen] = useState(false) - const { instanceId } = useParams<{ instanceId: string }>() const dispatch = useDispatch() useEffect(() => { - updateList() - }, []) + if (isModuleLoaded) { + updateList() + } + }, [modules]) useEffect(() => { applyFiltering() }, [filterValue, libraries]) + useEffect(() => + // componentWillUnmount + () => { + dispatch(setAddLibraryFormOpen(false)) + }, + []) + const handleSuccessUpdateList = (data: TriggeredFunctionsLibrary[]) => { if (selectedRow) { const findRow = find(data, (item) => item.name === selectedRow) @@ -83,7 +98,7 @@ const LibrariesPage = () => { } const handleSelectRow = (name?: string) => { - setIsAddLibraryPanelOpen(false) + dispatch(setAddLibraryFormOpen(false)) setSelectedRow(name ?? null) if (name !== selectedRow) { @@ -115,7 +130,7 @@ const LibrariesPage = () => { const onAddLibrary = () => { setSelectedRow(null) - setIsAddLibraryPanelOpen(true) + dispatch(setAddLibraryFormOpen(true)) sendEventTelemetry({ event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LOAD_LIBRARY_CLICKED, eventData: { @@ -125,7 +140,7 @@ const LibrariesPage = () => { } const onCloseAddLibrary = () => { - setIsAddLibraryPanelOpen(false) + dispatch(setAddLibraryFormOpen(false)) sendEventTelemetry({ event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LOAD_LIBRARY_CANCELLED, eventData: { @@ -135,12 +150,27 @@ const LibrariesPage = () => { } const onAdded = (libraryName: string) => { - setIsAddLibraryPanelOpen(false) setSelectedRow(libraryName) } const isRightPanelOpen = !isNull(selectedRow) || isAddLibraryPanelOpen + const isModuleLoaded = isTriggeredAndFunctionsAvailable(modules) + + const message = libraries?.length + ? NoLibrariesMessage + : ( + + ) + + if (!instanceId) { + return null + } + return ( { className="triggeredFunctions__topPanel" > - {!!libraries?.length && ( - - )} + - - + Library - + + + Library + + @@ -204,20 +240,17 @@ const LibrariesPage = () => {
    )} - {!!libraries?.length && ( - - )} - {libraries?.length === 0 && ( - - )} +
    { } }) }) + + it('should render disabled auto refresh btn', () => { + render() + + expect(screen.getByTestId('refresh-libraries-btn')).toBeDisabled() + }) + + it('should call proper telemetry events when sorting is changed', () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + const { container } = render() + + fireEvent.click(container.querySelector('[data-test-subj="tableHeaderSortButton"') as HTMLInputElement) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TRIGGERS_AND_FUNCTIONS_LIBRARIES_SORTED, + eventData: { + databaseId: 'instanceId', + direction: 'asc', + field: 'name', + } + }) + + sendEventTelemetry.mockRestore() + }) + + it('should open delete popover', () => { + render() + + fireEvent.click(screen.getByTestId('delete-library-icon-lib1')); + + (async () => { + await waitForEuiPopoverVisible() + })() + + expect(screen.getByTestId('delete-library-popover-lib1')).toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx index 0e0e0552c4..122f41e8c3 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/LibrariesList/LibrariesList.tsx @@ -24,12 +24,22 @@ export interface Props { selectedRow: Nullable onSelectRow: (name: string) => void onDeleteRow: (name: string) => void + message: React.ReactNode + isRefreshDisabled: boolean } -const NoLibrariesMessage: React.ReactNode = (No Libraries found) - const LibrariesList = (props: Props) => { - const { items, loading, onRefresh, lastRefresh, selectedRow, onSelectRow, onDeleteRow } = props + const { + items, + loading, + onRefresh, + lastRefresh, + selectedRow, + onSelectRow, + onDeleteRow, + message, + isRefreshDisabled, + } = props const [sort, setSort] = useState>(undefined) const [popover, setPopover] = useState>(null) @@ -160,6 +170,7 @@ const LibrariesList = (props: Props) => { onRefresh={() => onRefresh?.()} onRefreshClicked={handleRefreshClicked} onEnableAutoRefresh={handleEnableAutoRefresh} + disabled={isRefreshDisabled} testid="refresh-libraries-btn" /> @@ -174,10 +185,15 @@ const LibrariesList = (props: Props) => { className: row.name === selectedRow ? 'selected' : '', 'data-testid': `row-${row.name}`, })} - message={NoLibrariesMessage} + message={message} onTableChange={handleSorting} onWheel={handleClosePopover} - className={cx('inMemoryTableDefault', 'noBorders', 'triggeredFunctions__table')} + className={cx( + 'inMemoryTableDefault', + 'noBorders', + 'triggeredFunctions__table', + { triggeredFunctions__emptyTable: !items?.length } + )} data-testid="libraries-list-table" /> diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx deleted file mode 100644 index fad1443b19..0000000000 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.spec.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' -import { instance, mock } from 'ts-mockito' -import { render, fireEvent, screen } from 'uiSrc/utils/test-utils' - -import NoLibrariesScreen, { IProps } from './NoLibrariesScreen' - -const mockedProps = mock() - -describe('NoLibrariesScreen', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) - - it('should call onAddLibrary', () => { - const onAddLibrary = jest.fn() - - render() - - fireEvent.click(screen.getByTestId('add-library-no-libraries-btn')) - - expect(onAddLibrary).toHaveBeenCalled() - }) -}) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.tsx deleted file mode 100644 index 3c6294a391..0000000000 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/NoLibrariesScreen.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' - -import { EuiIcon, EuiTitle, EuiText, EuiSpacer, EuiButton } from '@elastic/eui' -import { ReactComponent as WelcomeIcon } from 'uiSrc/assets/img/icons/welcome.svg' - -import styles from './styles.module.scss' - -export interface IProps { - onAddLibrary: () => void -} - -const NoLibrariesScreen = ({ onAddLibrary }: IProps) => ( -
    -
    - - - -

    Triggers and Functions

    -
    - - See an overview of triggers and functions uploaded, upload new libraries, and manage the list of existing ones. - - To start working with triggers and functions, click - - + Library - - to upload a new library. - -
    -
    -) - -export default NoLibrariesScreen diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss deleted file mode 100644 index 256e0c9068..0000000000 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/components/NoLibrariesScreen/styles.module.scss +++ /dev/null @@ -1,30 +0,0 @@ -@import '@elastic/eui/src/global_styling/mixins/helpers'; -@import '@elastic/eui/src/global_styling/index'; - -.wrapper { - height: 100%; - width: 100%; - - @include euiScrollBar; - overflow: auto; - - padding: 40px; - display: flex; - justify-content: center; - align-items: center; -} - -.container { - text-align: center; - - max-width: 560px; - margin: 0 auto; -} - -.icon { - color: transparent; -} - -.btn { - margin: 0 6px; -} diff --git a/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss b/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss index afdcfb9277..d58039e657 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss +++ b/redisinsight/ui/src/pages/triggeredFunctions/styles.modules.scss @@ -138,6 +138,22 @@ $breakpoint-to-hide-resize-panel: 1124px; } } + &__emptyTable { + .euiTableCellContent__text { + pointer-events: auto !important; + } + + tbody .euiTableCellContent span { + white-space: break-spaces; + width: 100%; + } + + .euiTableRow:hover, + .euiTableRow:focus { + background-color: inherit !important; + } + } + &__closeRightPanel { position: absolute; top: 16px; diff --git a/redisinsight/ui/src/pages/workbench/constants.ts b/redisinsight/ui/src/pages/workbench/constants.ts index 571edad511..45921ed825 100644 --- a/redisinsight/ui/src/pages/workbench/constants.ts +++ b/redisinsight/ui/src/pages/workbench/constants.ts @@ -66,4 +66,5 @@ export enum ModuleCommandPrefix { CMS = 'CMS.', TOPK = 'TOPK.', TDIGEST = 'TDIGEST.', + TriggersAndFunctions = 'TF', } diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index ed6e9706b0..9db2ff649f 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -189,10 +189,11 @@ export const COMMAND_MODULES = { [RedisDefaultModules.ReJSON]: [RedisDefaultModules.ReJSON], [RedisDefaultModules.TimeSeries]: [RedisDefaultModules.TimeSeries], [RedisDefaultModules.Bloom]: [RedisDefaultModules.Bloom], + [RedisDefaultModules.RedisGears]: TRIGGERED_AND_FUNCTIONS_MODULES, } const RediSearchModulesText = [...REDISEARCH_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'RediSearch' }), {}) -const TriggeredAndFunctionsModulesText = [...TRIGGERED_AND_FUNCTIONS_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'Triggers & Functions' }), {}) +const TriggeredAndFunctionsModulesText = [...TRIGGERED_AND_FUNCTIONS_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'Triggers and Functions' }), {}) // Enums don't allow to use dynamic key export const DATABASE_LIST_MODULES_TEXT = Object.freeze({ diff --git a/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts b/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts index 330b2e6b3e..fb58e67b5d 100644 --- a/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts +++ b/redisinsight/ui/src/slices/interfaces/triggeredFunctions.ts @@ -67,6 +67,7 @@ export interface StateTriggeredFunctions { loading: boolean } addLibrary: { + open: boolean loading: boolean } } diff --git a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts index 5a792029cc..5307d69afe 100644 --- a/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts +++ b/redisinsight/ui/src/slices/tests/triggeredFunctions/triggeredFunctions.spec.ts @@ -30,6 +30,7 @@ import reducer, { addTriggeredFunctionsLibrarySuccess, addTriggeredFunctionsLibraryFailure, addTriggeredFunctionsLibraryAction, + setAddLibraryFormOpen, } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' import { apiService } from 'uiSrc/services' import { addMessageNotification, addErrorNotification } from 'uiSrc/slices/app/notifications' @@ -505,6 +506,7 @@ describe('triggeredFunctions slice', () => { const state = { ...initialState, addLibrary: { + open: false, loading: true } } @@ -553,12 +555,14 @@ describe('triggeredFunctions slice', () => { const currentState = { ...initialState, addLibrary: { + ...initialState.addLibrary, loading: true, }, } const state = { ...initialState, addLibrary: { + ...initialState.addLibrary, loading: false, }, } @@ -573,6 +577,35 @@ describe('triggeredFunctions slice', () => { expect(triggeredFunctionsSelector(rootState)).toEqual(state) }) }) + + describe('setAddLibraryFormOpen', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + addLibrary: { + ...initialState.addLibrary, + open: false, + }, + } + const state = { + ...initialState, + addLibrary: { + ...initialState.addLibrary, + open: true, + }, + } + + // Act + const nextState = reducer(currentState, setAddLibraryFormOpen(true)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + triggeredFunctions: nextState, + }) + expect(triggeredFunctionsSelector(rootState)).toEqual(state) + }) + }) }) // thunks @@ -692,6 +725,7 @@ describe('triggeredFunctions slice', () => { const expectedActions = [ getTriggeredFunctionsLibraryDetails(), getTriggeredFunctionsLibraryDetailsSuccess(data), + setAddLibraryFormOpen(false) ] expect(store.getActions()).toEqual(expectedActions) diff --git a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts index f0baf40712..c7d7c4dc69 100644 --- a/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts +++ b/redisinsight/ui/src/slices/triggeredFunctions/triggeredFunctions.ts @@ -37,6 +37,7 @@ export const initialState: StateTriggeredFunctions = { data: null }, addLibrary: { + open: false, loading: false, }, } @@ -124,6 +125,9 @@ const triggeredFunctionsSlice = createSlice({ addTriggeredFunctionsLibraryFailure: (state) => { state.addLibrary.loading = false }, + setAddLibraryFormOpen: (state, { payload }: PayloadAction) => { + state.addLibrary.open = payload + } } }) @@ -150,6 +154,7 @@ export const { addTriggeredFunctionsLibrary, addTriggeredFunctionsLibrarySuccess, addTriggeredFunctionsLibraryFailure, + setAddLibraryFormOpen, } = triggeredFunctionsSlice.actions export const triggeredFunctionsSelector = (state: RootState) => state.triggeredFunctions @@ -243,6 +248,7 @@ export function fetchTriggeredFunctionsLibrary( if (isStatusSuccessful(status)) { dispatch(getTriggeredFunctionsLibraryDetailsSuccess(data)) + dispatch(setAddLibraryFormOpen(false)) onSuccessAction?.(data) } } catch (_err) { diff --git a/redisinsight/ui/src/telemetry/interfaces.ts b/redisinsight/ui/src/telemetry/interfaces.ts index a635183707..6bf40dcf75 100644 --- a/redisinsight/ui/src/telemetry/interfaces.ts +++ b/redisinsight/ui/src/telemetry/interfaces.ts @@ -52,7 +52,7 @@ export enum RedisModules { RedisJSON = 'ReJSON', RediSearch = 'search', RedisTimeSeries = 'timeseries', - 'Triggers & Functions' = 'redisgears' + 'Triggers and Functions' = 'redisgears' } export interface IModuleSummary { diff --git a/redisinsight/ui/src/telemetry/pageViews.ts b/redisinsight/ui/src/telemetry/pageViews.ts index 72b2e9ac39..7515f2af01 100644 --- a/redisinsight/ui/src/telemetry/pageViews.ts +++ b/redisinsight/ui/src/telemetry/pageViews.ts @@ -8,5 +8,5 @@ export enum TelemetryPageView { CLUSTER_DETAILS_PAGE = 'Overview', PUBSUB_PAGE = 'Pub/Sub', DATABASE_ANALYSIS = 'Database Analysis', - TRIGGERED_FUNCTIONS = 'Triggers & Functions' + TRIGGERED_FUNCTIONS = 'Triggers and Functions' } diff --git a/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts b/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts index 95cd572f8a..e443b1b291 100644 --- a/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts +++ b/redisinsight/ui/src/telemetry/telemetryUtils.spec.ts @@ -9,7 +9,7 @@ const DEFAULT_SUMMARY = Object.freeze( RedisBloom: { loaded: false }, RedisJSON: { loaded: false }, RedisTimeSeries: { loaded: false }, - 'Triggers & Functions': { loaded: false }, + 'Triggers and Functions': { loaded: false }, customModules: [], }, ) @@ -54,7 +54,7 @@ const getRedisModulesSummaryTests = [ RedisJSON: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RediSearch: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, RedisTimeSeries: { loaded: true, version: 10000, semanticVersion: '1.0.0' }, - 'Triggers & Functions': { loaded: true, version: 10000, semanticVersion: '1.0.0' }, + 'Triggers and Functions': { loaded: true, version: 10000, semanticVersion: '1.0.0' }, customModules: [], }, }, diff --git a/redisinsight/ui/src/telemetry/telemetryUtils.ts b/redisinsight/ui/src/telemetry/telemetryUtils.ts index 99299f7e5b..e131012a40 100644 --- a/redisinsight/ui/src/telemetry/telemetryUtils.ts +++ b/redisinsight/ui/src/telemetry/telemetryUtils.ts @@ -220,7 +220,7 @@ const DEFAULT_SUMMARY: IRedisModulesSummary = Object.freeze( RedisBloom: { loaded: false }, RedisJSON: { loaded: false }, RedisTimeSeries: { loaded: false }, - 'Triggers & Functions': { loaded: false }, + 'Triggers and Functions': { loaded: false }, customModules: [], }, ) @@ -254,7 +254,7 @@ const getRedisModulesSummary = (modules: AdditionalRedisModule[] = []): IRedisMo } if (isTriggeredAndFunctionsAvailable([module])) { - const triggeredAndFunctionsName = getEnumKeyBValue(RedisModules, RedisModules['Triggers & Functions']) + const triggeredAndFunctionsName = getEnumKeyBValue(RedisModules, RedisModules['Triggers and Functions']) summary[triggeredAndFunctionsName as RedisModulesKeyType] = getModuleSummaryToSent(module) return } diff --git a/redisinsight/ui/src/utils/cliHelper.tsx b/redisinsight/ui/src/utils/cliHelper.tsx index 3bc6ce08b6..28876db09d 100644 --- a/redisinsight/ui/src/utils/cliHelper.tsx +++ b/redisinsight/ui/src/utils/cliHelper.tsx @@ -169,6 +169,9 @@ const checkCommandModule = (command: string) => { case command.startsWith(ModuleCommandPrefix.TOPK): { return RedisDefaultModules.Bloom } + case command.startsWith(ModuleCommandPrefix.TriggersAndFunctions): { + return RedisDefaultModules.RedisGears + } default: { return null } diff --git a/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts b/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts index f75884595b..56e0eba9a9 100644 --- a/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts +++ b/tests/e2e/pageObjects/triggers-and-functions-functions-page.ts @@ -5,6 +5,7 @@ export class TriggersAndFunctionsFunctionsPage extends InstancePage { //Links librariesLink = Selector('[data-testid=triggered-functions-tab-libraries]'); + noLibrariesLink = Selector('[data-testid=no-libraries-title]'); //Buttons invokeButton = Selector('[data-testid=invoke-btn]'); diff --git a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts index 1aa500058d..c16491d1b1 100644 --- a/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts +++ b/tests/e2e/tests/critical-path/triggers-and-functions/libraries.e2e.ts @@ -141,6 +141,7 @@ test('Verify that library can be uploaded', async t => { const libNameFromFile = 'lib'; await t.click(browserPage.NavigationPanel.triggeredFunctionsButton); + await t.expect(triggersAndFunctionsFunctionsPage.noLibrariesLink.exists).ok('no libraries title is displayed'); await t.click(triggersAndFunctionsFunctionsPage.librariesLink); await t.click(triggersAndFunctionsLibrariesPage.addLibraryButton); await t.setFilesToUpload(triggersAndFunctionsLibrariesPage.uploadInput, [filePathes.upload]); diff --git a/tests/e2e/tests/regression/browser/onboarding.e2e.ts b/tests/e2e/tests/regression/browser/onboarding.e2e.ts index ffa08d2f37..c98ae75824 100644 --- a/tests/e2e/tests/regression/browser/onboarding.e2e.ts +++ b/tests/e2e/tests/regression/browser/onboarding.e2e.ts @@ -113,7 +113,7 @@ test('Verify onboarding new user steps', async t => { await onboardingCardsDialog.clickNextStep(); // verify triggered and functions page is opened await t.expect(functionsPage.librariesLink.visible).ok('triggered and functions page is not opened'); - await onboardingCardsDialog.verifyStepVisible('Triggers & Functions'); + await onboardingCardsDialog.verifyStepVisible('Triggers and Functions'); await onboardingCardsDialog.clickNextStep(); // verify last step of onboarding process is visible await onboardingCardsDialog.verifyStepVisible('Great job!'); From aab7151ca04cb9c6d843ae4234afdf73c9108332 Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:58:16 +0300 Subject: [PATCH 090/106] #RI-4770 - add icons and update text (#2370) --- .../img/triggers_and_functions_dark.svg | 63 +++++++++++++++++++ .../img/triggers_and_functions_light.svg | 63 +++++++++++++++++++ .../ui/src/constants/workbenchResults.ts | 2 +- .../NoLibrariesScreen/NoLibrariesScreen.tsx | 14 +++-- 4 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg create mode 100644 redisinsight/ui/src/assets/img/triggers_and_functions_light.svg diff --git a/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg b/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg new file mode 100644 index 0000000000..c8956e53a1 --- /dev/null +++ b/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg b/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg new file mode 100644 index 0000000000..c17a525dac --- /dev/null +++ b/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/constants/workbenchResults.ts b/redisinsight/ui/src/constants/workbenchResults.ts index 4da6b35df4..fca59a87f9 100644 --- a/redisinsight/ui/src/constants/workbenchResults.ts +++ b/redisinsight/ui/src/constants/workbenchResults.ts @@ -51,7 +51,7 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = improvements: [ 'Speed up applications by running the application logic where the data lives', 'Eliminate the need to maintain the same code across different applications by moving application functionality inside the Redis database', - 'Maintain consistent data when applications react to changing real-time conditions in the keyspace instead of using Pub/Sub notifications', + 'Maintain consistent data when applications react to any keyspace change', 'Improve code resiliency by backing up and replicating triggers and functions along with the database' ], additionalText: ['Triggers and functions work with a JavaScript engine, which lets you take advantage of JavaScript’s vast ecosystem of libraries and frameworks and modern, expressive syntax.'], diff --git a/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.tsx b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.tsx index 982c6686fb..82633cd08a 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/NoLibrariesScreen.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useContext } from 'react' import { useSelector, useDispatch } from 'react-redux' import { useParams, useHistory } from 'react-router-dom' import cx from 'classnames' @@ -9,14 +9,15 @@ import { EuiButton, EuiButtonEmpty, EuiLink, - EuiIcon, } from '@elastic/eui' +import { ThemeContext } from 'uiSrc/contexts/themeContext' +import { Theme, EAManifestFirstKey, Pages, MODULE_NOT_LOADED_CONTENT as CONTENT, MODULE_TEXT_VIEW } from 'uiSrc/constants' import { workbenchGuidesSelector } from 'uiSrc/slices/workbench/wb-guides' import { resetWorkbenchEASearch, setWorkbenchEAMinimized } from 'uiSrc/slices/app/context' -import { EAManifestFirstKey, Pages, MODULE_NOT_LOADED_CONTENT as CONTENT, MODULE_TEXT_VIEW } from 'uiSrc/constants' import { ReactComponent as CheerIcon } from 'uiSrc/assets/img/icons/cheer.svg' -import TriggersAndFunctionsImage from 'uiSrc/assets/img/onboarding-emoji.svg' +import { ReactComponent as TriggersAndFunctionsImageDark } from 'uiSrc/assets/img/triggers_and_functions_dark.svg' +import { ReactComponent as TriggersAndFunctionsImageLight } from 'uiSrc/assets/img/triggers_and_functions_light.svg' import { RedisDefaultModules } from 'uiSrc/slices/interfaces' import { findMarkdownPathByPath } from 'uiSrc/utils' @@ -46,6 +47,7 @@ const NoLibrariesScreen = (props: IProps) => { const { instanceId = '' } = useParams<{ instanceId: string }>() const dispatch = useDispatch() const history = useHistory() + const { theme } = useContext(ThemeContext) const goToTutorial = () => { // triggers and functions tutorial does not upload @@ -137,7 +139,9 @@ const NoLibrariesScreen = (props: IProps) => { {!isAddLibraryPanelOpen && (
    - + {theme === Theme.Dark + ? + : }
    )} From 3e4b06ff487c0170c90e644083f4143199230c20 Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Thu, 20 Jul 2023 12:30:48 +0200 Subject: [PATCH 091/106] #RI-4768 - fix color --- .../components/recommendation/styles.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss index 3612a84331..38317baa17 100644 --- a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss +++ b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss @@ -1,5 +1,5 @@ :global(.euiAccordion__childWrapper) { - background-color: var(--recommendationBgColor); + background-color: var(--recommendationBgColor) !important; } .recommendationAccordion { From 3cbf8878b4e9b52cd6d792a3f78890ac836ccaaa Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 20 Jul 2023 13:01:25 +0200 Subject: [PATCH 092/106] fixes for unstable tests --- .../critical-path/database/modules.e2e.ts | 28 +++++++++++++------ .../memory-efficiency/recommendations.e2e.ts | 8 +++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/tests/e2e/tests/critical-path/database/modules.e2e.ts b/tests/e2e/tests/critical-path/database/modules.e2e.ts index 3ad853f128..4e5dab8945 100644 --- a/tests/e2e/tests/critical-path/database/modules.e2e.ts +++ b/tests/e2e/tests/critical-path/database/modules.e2e.ts @@ -1,3 +1,4 @@ +import { Chance } from 'chance'; import { Selector } from 'testcafe'; import { rte, env } from '../../../helpers/constants'; import { DatabaseHelper } from '../../../helpers/database'; @@ -9,35 +10,46 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const chance = new Chance(); const moduleNameList = ['RediSearch', 'RedisJSON', 'RedisGraph', 'RedisTimeSeries', 'RedisBloom', 'RedisGears', 'RedisAI']; const moduleList = [myRedisDatabasePage.moduleSearchIcon, myRedisDatabasePage.moduleJSONIcon, myRedisDatabasePage.moduleGraphIcon, myRedisDatabasePage.moduleTimeseriesIcon, myRedisDatabasePage.moduleBloomIcon, myRedisDatabasePage.moduleGearsIcon, myRedisDatabasePage.moduleAIIcon]; +const uniqueId = chance.string({ length: 10 }); +let database = { + ...ossStandaloneRedisearch, + databaseName: `test_standalone-redisearch-${uniqueId}` +}; fixture `Database modules` .meta({ type: 'critical_path' }) .page(commonUrl) .beforeEach(async() => { await databaseHelper.acceptLicenseTerms(); - await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneRedisearch); + database = { + ...ossStandaloneRedisearch, + databaseName: `test_standalone-redisearch-${uniqueId}` + }; + await databaseAPIRequests.addNewStandaloneDatabaseApi(database); // Reload Page await browserPage.reloadPage(); }) .afterEach(async() => { // Delete database - await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + await databaseAPIRequests.deleteStandaloneDatabaseApi(database); }); test .meta({ rte: rte.standalone, env: env.web })('Verify that user can see DB modules on DB list page for Standalone DB', async t => { // Check module column on DB list page await t.expect(myRedisDatabasePage.moduleColumn.exists).ok('Module column not found'); // Verify that user can see the following sorting order: Search, JSON, Graph, TimeSeries, Bloom, Gears, AI for modules - const databaseLine = myRedisDatabasePage.dbNameList.withExactText(ossStandaloneRedisearch.databaseName).parent('tr'); + const databaseLine = myRedisDatabasePage.dbNameList.withExactText(database.databaseName).parent('tr'); await t.expect(databaseLine.visible).ok('Database not found in db list'); const moduleIcons = databaseLine.find('[data-testid^=Redi]'); - const numberOfIcons = moduleIcons.count; - for (let i = 0; i < await numberOfIcons; i++) { - const moduleName = moduleIcons.nth(i).getAttribute('data-testid'); - await t.expect(moduleName).eql(await moduleList[i].getAttribute('data-testid'), `${moduleName} icon not found`); + const numberOfIcons = await moduleIcons.count; + for (let i = 0; i < numberOfIcons; i++) { + const moduleName = await moduleIcons.nth(i).getAttribute('data-testid'); + const expectedName = await moduleList[i].getAttribute('data-testid'); + await t.expect(moduleName).eql(expectedName, `${moduleName} icon not found`); } //Minimize the window to check quantifier await t.resizeWindow(1000, 700); @@ -64,7 +76,7 @@ test test .meta({ rte: rte.standalone })('Verify that user can see icons in DB header for RediSearch, RedisGraph, RedisJSON, RedisBloom, RedisTimeSeries, RedisGears, RedisAI default modules', async t => { // Connect to DB - await myRedisDatabasePage.clickOnDBByName(ossStandaloneRedisearch.databaseName); + await myRedisDatabasePage.clickOnDBByName(database.databaseName); // Check all available modules in overview const moduleIcons = Selector('div').find('[data-testid^=Redi]'); const numberOfIcons = await moduleIcons.count; diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index 8130d56c98..cce5df6b9e 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -174,8 +174,14 @@ test await browserPage.deleteKeyByName(keyName); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can see the Tutorial opened when clicking on "Tutorial" for recommendations', async t => { + const recommendation = memoryEfficiencyPage.getRecommendationByName(searchJsonRecommendation); + for (let i = 0; i < 5; i++) { + if (!(await recommendation.exists)) { + await t.click(memoryEfficiencyPage.newReportBtn); + } + } // Verify that Optimize the use of time series recommendation displayed - await t.expect(await memoryEfficiencyPage.getRecommendationByName(searchJsonRecommendation).exists).ok('Query and search JSON documents recommendation not displayed'); + await t.expect(recommendation.exists).ok('Query and search JSON documents recommendation not displayed'); // Verify that tutorial opened await t.click(memoryEfficiencyPage.getToTutorialBtnByRecomName(searchJsonRecommendation)); await t.expect(workbenchPage.preselectArea.visible).ok('Workbench Enablement area not opened'); From 70380c78137135229b6f3965cca77b71c3b0d56f Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Thu, 20 Jul 2023 13:18:54 +0200 Subject: [PATCH 093/106] #RI-4771 - New recommendation is not highlighted --- .../components/recommendation/styles.module.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss index 38317baa17..407de7675e 100644 --- a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss +++ b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss @@ -6,6 +6,11 @@ margin-bottom: 10px; border-radius: 8px; overflow: hidden; + border: 1px solid var(--recommendationLiveBorderColor); + + &.read { + border: 1px solid var(--recommendationBgColor); + } .redisStackLink { margin-right: 12px; @@ -168,7 +173,7 @@ :global(.euiButton__text) { color: var(--euiColorEmptyShade); font: - normal normal 400 14px/17px Graphik, + normal normal 600 14px/17px Graphik, sans-serif !important; } } From 1b1fa2f8b1a70520751d089bd93ab3588d511610 Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Thu, 20 Jul 2023 13:25:50 +0200 Subject: [PATCH 094/106] #RI-4771 - New recommendation is not highlighted --- .../components/recommendation/styles.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss index 407de7675e..c648f0cbcc 100644 --- a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss +++ b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss @@ -173,7 +173,7 @@ :global(.euiButton__text) { color: var(--euiColorEmptyShade); font: - normal normal 600 14px/17px Graphik, + normal normal 500 14px/17px Graphik, sans-serif !important; } } From e776726f3665b6ad5f164e0847ee9f9bf237935f Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Thu, 20 Jul 2023 13:38:19 +0200 Subject: [PATCH 095/106] #RI-4772 - DB recommendation has unexpected background for white theme --- .../components/recommendation/styles.module.scss | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss index c648f0cbcc..980323dac7 100644 --- a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss +++ b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/styles.module.scss @@ -1,7 +1,3 @@ -:global(.euiAccordion__childWrapper) { - background-color: var(--recommendationBgColor) !important; -} - .recommendationAccordion { margin-bottom: 10px; border-radius: 8px; @@ -17,6 +13,9 @@ } :global { + .euiAccordion__childWrapper { + background-color: var(--recommendationBgColor) !important; + } .euiAccordion__button { padding: 6px 18px; border-bottom: 1px solid transparent; From 20e531c00c5043d5e49e289f94ff230a163b4830 Mon Sep 17 00:00:00 2001 From: Amir Allayarov <100589048+AmirAllayarovSofteq@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:44:08 +0300 Subject: [PATCH 096/106] #RI-4579 - update icons (#2372) --- .../img/triggers_and_functions_dark.svg | 112 ++++++++--------- .../img/triggers_and_functions_light.svg | 117 +++++++++--------- .../NoLibrariesScreen/styles.module.scss | 1 + 3 files changed, 118 insertions(+), 112 deletions(-) diff --git a/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg b/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg index c8956e53a1..198f36f814 100644 --- a/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg +++ b/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg @@ -1,63 +1,63 @@ - - - - + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg b/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg index c17a525dac..5ae9fe1e06 100644 --- a/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg +++ b/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg @@ -1,63 +1,68 @@ - - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/styles.module.scss b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/styles.module.scss index d0ab24b3c6..3d17894e80 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/styles.module.scss +++ b/redisinsight/ui/src/pages/triggeredFunctions/components/NoLibrariesScreen/styles.module.scss @@ -17,6 +17,7 @@ width: 50%; .image { + padding-top: 10px; width: 100%; } } From f4283c8cc795e49bcf5dbb123933de25d06d68b9 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 20 Jul 2023 14:18:34 +0200 Subject: [PATCH 097/106] #RI-4626 - remove text --- .../ui/src/components/notifications/success-messages.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/redisinsight/ui/src/components/notifications/success-messages.tsx b/redisinsight/ui/src/components/notifications/success-messages.tsx index 861f281e72..5cd4e659b1 100644 --- a/redisinsight/ui/src/components/notifications/success-messages.tsx +++ b/redisinsight/ui/src/components/notifications/success-messages.tsx @@ -58,8 +58,7 @@ export default { <> {formatNameShort(bufferToString(keyName))} {' '} - has been added. Please refresh the list of Keys to see - updates. + has been added. ), }), From ca73ba312e0d22cf795eb713c89311bc081a5303 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 20 Jul 2023 15:52:33 +0200 Subject: [PATCH 098/106] release 2.30.0 candidate --- electron-builder.json | 2 +- redisinsight/api/config/default.ts | 2 +- redisinsight/api/config/swagger.ts | 2 +- redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts | 2 +- redisinsight/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/electron-builder.json b/electron-builder.json index c72a699725..e267ab26fa 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -29,7 +29,7 @@ "type": "distribution", "hardenedRuntime": true, "darkModeSupport": true, - "bundleVersion": "50", + "bundleVersion": "60", "icon": "resources/icon.icns", "artifactName": "${productName}-${os}-${arch}.${ext}", "entitlements": "resources/entitlements.mac.plist", diff --git a/redisinsight/api/config/default.ts b/redisinsight/api/config/default.ts index 6c3af8d7b1..6b0cc6990b 100644 --- a/redisinsight/api/config/default.ts +++ b/redisinsight/api/config/default.ts @@ -58,7 +58,7 @@ export default { tlsKey: process.env.SERVER_TLS_KEY, staticContent: !!process.env.SERVER_STATIC_CONTENT || false, buildType: process.env.BUILD_TYPE || 'ELECTRON', - appVersion: process.env.APP_VERSION || '2.28.1', + appVersion: process.env.APP_VERSION || '2.30.0', requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 25000, excludeRoutes: [], excludeAuthRoutes: [], diff --git a/redisinsight/api/config/swagger.ts b/redisinsight/api/config/swagger.ts index 0cf2aa7836..c1a63c58bc 100644 --- a/redisinsight/api/config/swagger.ts +++ b/redisinsight/api/config/swagger.ts @@ -5,7 +5,7 @@ const SWAGGER_CONFIG: Omit = { info: { title: 'RedisInsight Backend API', description: 'RedisInsight Backend API', - version: '2.28.1', + version: '2.30.0', }, tags: [], }; diff --git a/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts b/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts index 4c24204095..82adf48856 100644 --- a/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts +++ b/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts @@ -8,7 +8,7 @@ const ICON_PATH = app.isPackaged export const AboutPanelOptions = { applicationName: 'RedisInsight-v2', - applicationVersion: `${app.getVersion() || '2.28.1'}${ + applicationVersion: `${app.getVersion() || '2.30.0'}${ !config.isProduction ? `-dev-${process.getCreationTime()}` : '' }`, copyright: `Copyright © ${new Date().getFullYear()} Redis Ltd.`, diff --git a/redisinsight/package.json b/redisinsight/package.json index 8d9e6d9ff0..90ae4bcc1e 100644 --- a/redisinsight/package.json +++ b/redisinsight/package.json @@ -2,7 +2,7 @@ "name": "redisinsight", "productName": "RedisInsight", "private": true, - "version": "2.28.1", + "version": "2.30.0", "description": "RedisInsight", "main": "./dist/main/main.js", "author": { From c0613d967bacdfb4b3222a9157e279eafab285d9 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 20 Jul 2023 17:09:51 +0200 Subject: [PATCH 099/106] updates after RI-4601 --- .../critical-path/browser/search-capabilities.e2e.ts | 2 +- .../workbench/redisearch-module-not-available.e2e.ts | 2 +- .../workbench/redisearch-module-not-available.e2e.ts | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 5f09b0a093..2356f9f7e5 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -150,7 +150,7 @@ test .after(async() => { await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); })('No RediSearch module message', async t => { - const noRedisearchMessage = 'Looks like RediSearch is not available for this database'; + const noRedisearchMessage = 'RediSearch is not available for this database'; // const externalPageLink = 'https://redis.com/try-free/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_browser_search'; await t.click(browserPage.redisearchModeBtn); diff --git a/tests/e2e/tests/critical-path/workbench/redisearch-module-not-available.e2e.ts b/tests/e2e/tests/critical-path/workbench/redisearch-module-not-available.e2e.ts index d0b26dfc90..2d9dcc7ebe 100644 --- a/tests/e2e/tests/critical-path/workbench/redisearch-module-not-available.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/redisearch-module-not-available.e2e.ts @@ -27,5 +27,5 @@ test('Verify that user can see the information message that the RediSearch modul // Send command with 'FT.' await workbenchPage.sendCommandInWorkbench(commandForSend); // Verify the information message - await t.expect(await workbenchPage.commandExecutionResult.textContent).contains('Looks like RediSearch is not available', 'The information message'); + await t.expect(await workbenchPage.commandExecutionResult.textContent).contains('RediSearch is not available for this database', 'The information message'); }); diff --git a/tests/e2e/tests/regression/workbench/redisearch-module-not-available.e2e.ts b/tests/e2e/tests/regression/workbench/redisearch-module-not-available.e2e.ts index 24f0aee100..0d254edf3f 100644 --- a/tests/e2e/tests/regression/workbench/redisearch-module-not-available.e2e.ts +++ b/tests/e2e/tests/regression/workbench/redisearch-module-not-available.e2e.ts @@ -52,11 +52,11 @@ test const commandJSON = 'JSON.ARRAPPEND key value'; const commandFT = 'FT.LIST'; await workbenchPage.sendCommandInWorkbench(commandJSON); - // Verify change screens when capability not available - 'Search' - await t.expect(workbenchPage.welcomePageTitle.withText('Looks like RedisJSON is not available').visible) + // Verify change screens when capability not available - 'JSON' + await t.expect(await workbenchPage.commandExecutionResult.withText('RedisJSON is not available for this database').visible) .ok('Missing RedisJSON title is not visible'); await workbenchPage.sendCommandInWorkbench(commandFT); - // Verify change screens when capability not available - 'JSON' - await t.expect(workbenchPage.welcomePageTitle.withText('Looks like RediSearch is not available').visible) - .ok('Missing RedisJSON title is not visible'); + // Verify change screens when capability not available - 'Search' + await t.expect(await workbenchPage.commandExecutionResult.withText('RediSearch is not available for this database').visible) + .ok('Missing RedisSearch title is not visible'); }); From 5fa9a2ef44b19e8083695b8ddc6d04a15caa5089 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 24 Jul 2023 11:58:06 +0300 Subject: [PATCH 100/106] store docker artifacts --- .circleci/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index c93dd876fe..a6ad04196c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -790,6 +790,9 @@ jobs: root: . paths: - ./docker-release + - store_artifacts: + path: ./docker-release + destination: ./docker-release # Release jobs store-build-artifacts: From 919907c725b19fe2904a29afb3cc8f8bc99ddcdb Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 24 Jul 2023 11:59:06 +0300 Subject: [PATCH 101/106] rename destination --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a6ad04196c..5a4651e287 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -791,8 +791,8 @@ jobs: paths: - ./docker-release - store_artifacts: - path: ./docker-release - destination: ./docker-release + path: docker-release + destination: docker-release # Release jobs store-build-artifacts: From b791f99b494ea55deff41a938f34ca01c369a0dc Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 24 Jul 2023 14:24:25 +0800 Subject: [PATCH 102/106] #RI-4778 - fix loading functions and libraries list --- .../pages/Functions/FunctionsPage.spec.tsx | 10 ++++++++ .../pages/Functions/FunctionsPage.tsx | 24 ++++++++++--------- .../pages/Libraries/LibrariesPage.spec.tsx | 10 ++++++++ .../pages/Libraries/LibrariesPage.tsx | 24 ++++++++++--------- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx index 873e3b70b0..e86a7950c1 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.spec.tsx @@ -161,4 +161,14 @@ describe('FunctionsPage', () => { expect(pushMock) .toBeCalledWith(Pages.triggeredFunctionsLibraries('instanceId')) }) + + it('should not render functions list', () => { + (triggeredFunctionsFunctionsSelector as jest.Mock).mockReturnValueOnce({ + data: null, + loading: false + }) + const { queryByTestId } = render() + + expect(queryByTestId('total-functions')).not.toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx index 54bbd7c002..cd6a39f5ce 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx @@ -3,7 +3,7 @@ import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiResiza import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import cx from 'classnames' -import { find, pick } from 'lodash' +import { find, isNull, pick } from 'lodash' import { fetchTriggeredFunctionsFunctionsList, setSelectedFunctionToShow, @@ -164,16 +164,18 @@ const FunctionsPage = () => { )} - + {!isNull(functions) && ( + + )} { expect(screen.queryByTestId('lib-details-lib1')).not.toBeInTheDocument() }) + + it('should not render libraries list', () => { + (triggeredFunctionsLibrariesSelector as jest.Mock).mockReturnValueOnce({ + data: null, + loading: false + }) + const { queryByTestId } = render() + + expect(queryByTestId('total-libraries')).not.toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx index 5a53914b91..a5260f218b 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Libraries/LibrariesPage.tsx @@ -240,17 +240,19 @@ const LibrariesPage = () => { )} - + {!isNull(libraries) && ( + + )} Date: Mon, 24 Jul 2023 20:03:37 +0800 Subject: [PATCH 103/106] #RI-4778 - fix loading and image --- .../ui/src/assets/img/triggers_and_functions_dark.svg | 2 +- .../ui/src/assets/img/triggers_and_functions_light.svg | 2 +- .../triggeredFunctions/pages/Functions/FunctionsPage.tsx | 6 +++++- .../triggeredFunctions/pages/Libraries/LibrariesPage.tsx | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg b/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg index 198f36f814..01419faea7 100644 --- a/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg +++ b/redisinsight/ui/src/assets/img/triggers_and_functions_dark.svg @@ -1,4 +1,4 @@ - + diff --git a/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg b/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg index 5ae9fe1e06..7a26127835 100644 --- a/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg +++ b/redisinsight/ui/src/assets/img/triggers_and_functions_light.svg @@ -1,4 +1,4 @@ - + diff --git a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx index cd6a39f5ce..682b72908a 100644 --- a/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx +++ b/redisinsight/ui/src/pages/triggeredFunctions/pages/Functions/FunctionsPage.tsx @@ -114,6 +114,10 @@ const FunctionsPage = () => { ? NoFunctionsMessage : () + if (!instanceId) { + return null + } + return ( { )} - {!isNull(functions) && ( + {(!isModuleLoaded || !isNull(functions)) && ( { )} - {!isNull(libraries) && ( + {(!isModuleLoaded || !isNull(libraries)) && ( Date: Mon, 24 Jul 2023 21:55:18 +0200 Subject: [PATCH 104/106] update bundle version --- electron-builder.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron-builder.json b/electron-builder.json index e267ab26fa..dc87c24635 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -29,7 +29,7 @@ "type": "distribution", "hardenedRuntime": true, "darkModeSupport": true, - "bundleVersion": "60", + "bundleVersion": "70", "icon": "resources/icon.icns", "artifactName": "${productName}-${os}-${arch}.${ext}", "entitlements": "resources/entitlements.mac.plist", From edf41629d70b58572f2427ffa91b81b666cda6b4 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 25 Jul 2023 19:19:24 +0800 Subject: [PATCH 105/106] #RI-4780 - update links --- .../components/MoreInfoPopover/MoreInfoPopover.tsx | 2 +- .../messages/filter-not-available/FilterNotAvailable.tsx | 2 +- .../components/onboarding-features/OnboardingFeatures.tsx | 2 +- redisinsight/ui/src/constants/links.ts | 2 +- redisinsight/ui/src/constants/workbenchResults.ts | 8 ++++---- .../recommendations-view/Recommendations.spec.tsx | 2 +- .../InstanceForm/form-components/Messages.tsx | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/redisinsight/ui/src/components/database-overview/components/MoreInfoPopover/MoreInfoPopover.tsx b/redisinsight/ui/src/components/database-overview/components/MoreInfoPopover/MoreInfoPopover.tsx index cb23e1c82b..86336360f0 100644 --- a/redisinsight/ui/src/components/database-overview/components/MoreInfoPopover/MoreInfoPopover.tsx +++ b/redisinsight/ui/src/components/database-overview/components/MoreInfoPopover/MoreInfoPopover.tsx @@ -12,7 +12,7 @@ import { IMetric } from '../OverviewMetrics' import './styles.scss' import styles from './styles.module.scss' -const ModulesInfoText = 'More information about Redis modules can be found here.\nCreate a free Redis database with modules support on Redis Cloud.\n' +const ModulesInfoText = 'More information about Redis modules can be found here.\nCreate a free Redis database with modules support on Redis Cloud.\n' interface IProps { metrics: Array, diff --git a/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx b/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx index 0e2e8a1b12..f7d10e92a3 100644 --- a/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx +++ b/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx @@ -6,7 +6,7 @@ import RedisDbBlueIcon from 'uiSrc/assets/img/icons/redis_db_blue.svg' import styles from './styles.module.scss' const GET_STARTED_LINK = 'https://redis.com/try-free/?utm_source=redisinsight&utm_medium=main&utm_campaign=browser_filter' -const LEARN_MORE_LINK = 'https://redis.io/docs/stack/about/?utm_source=redisinsight&utm_medium=main&utm_campaign=browser_filter' +const LEARN_MORE_LINK = 'https://redis.io/docs/about/about-stack/?utm_source=redisinsight&utm_medium=main&utm_campaign=browser_filter' const FilterNotAvailable = () => (
    diff --git a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx index d0d29a9478..7f8c364cad 100644 --- a/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx +++ b/redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx @@ -212,7 +212,7 @@ const ONBOARDING_FEATURES = { Take advantage of syntax highlighting, intelligent auto-complete, and working with commands in editor mode. - Workbench visualizes complex Redis Stack data + Workbench visualizes complex Redis Stack data models such as documents, graphs, and time series. Or you can build your own visualization. diff --git a/redisinsight/ui/src/constants/links.ts b/redisinsight/ui/src/constants/links.ts index 01530e4648..8520927391 100644 --- a/redisinsight/ui/src/constants/links.ts +++ b/redisinsight/ui/src/constants/links.ts @@ -5,5 +5,5 @@ export const EXTERNAL_LINKS = { userSurvey: 'https://www.surveymonkey.com/r/redisinsight', recommendationFeedback: 'https://github.com/RedisInsight/RedisInsight/issues/new/choose', guidesRepo: 'https://github.com/RedisInsight/Tutorials', - redisStack: 'https://redis.io/docs/stack/', + redisStack: 'https://redis.io/docs/about/about-stack/', } diff --git a/redisinsight/ui/src/constants/workbenchResults.ts b/redisinsight/ui/src/constants/workbenchResults.ts index fca59a87f9..199523d21c 100644 --- a/redisinsight/ui/src/constants/workbenchResults.ts +++ b/redisinsight/ui/src/constants/workbenchResults.ts @@ -12,7 +12,7 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = 'Perform cross-time-series range and aggregation queries', 'Define compaction rules for economical retention of historical data' ], - link: 'https://redis.io/docs/stack/timeseries/' + link: 'https://redis.io/docs/data-types/timeseries/' }, [RedisDefaultModules.Search]: { text: ['RediSearch adds the capability to:'], @@ -22,7 +22,7 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = 'Full-text search' ], additionalText: ['These features enable multi-field queries, aggregation, exact phrase matching, numeric filtering, ', 'geo filtering and vector similarity semantic search on top of text queries.'], - link: 'https://redis.io/docs/stack/search/' + link: 'https://redis.io/docs/interact/search-and-query/' }, [RedisDefaultModules.ReJSON]: { text: ['RedisJSON adds the capability to:'], @@ -32,7 +32,7 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = 'Retrieve JSON documents' ], additionalText: ['RedisJSON also works seamlessly with RediSearch to let you index and query JSON documents.'], - link: 'https://redis.io/docs/stack/json/' + link: 'https://redis.io/docs/data-types/json/' }, [RedisDefaultModules.Bloom]: { text: ['RedisBloom adds a set of probabilistic data structures to Redis, including:'], @@ -44,7 +44,7 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = 'T-digest' ], additionalText: ['With this capability you can query streaming data without needing to store all the elements of the stream.'], - link: 'https://redis.io/docs/stack/bloom/' + link: 'https://redis.io/docs/data-types/probabilistic/bloom-filter/' }, [RedisDefaultModules.RedisGears]: { text: ['Triggers and functions add the capability to execute server-side functions that are triggered by events or data operations to:'], diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index bdfd5ecc21..99ba6e3532 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -430,7 +430,7 @@ describe('Recommendations', () => { render() expect(screen.queryByTestId('bigSets-redis-stack-link')).toBeInTheDocument() - expect(screen.queryByTestId('bigSets-redis-stack-link')).toHaveAttribute('href', 'https://redis.io/docs/stack/') + expect(screen.queryByTestId('bigSets-redis-stack-link')).toHaveAttribute('href', 'https://redis.io/docs/about/about-stack/') }) it('should render go tutorial button', () => { diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/form-components/Messages.tsx b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/form-components/Messages.tsx index 93a82e9156..cce51f1b2a 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/form-components/Messages.tsx +++ b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/form-components/Messages.tsx @@ -33,7 +33,7 @@ const MessageSentinel = () => ( .   Date: Tue, 25 Jul 2023 14:14:06 +0200 Subject: [PATCH 106/106] fix --- redisinsight/ui/src/electron/utils/ipcDeleteStoreValues.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/electron/utils/ipcDeleteStoreValues.ts b/redisinsight/ui/src/electron/utils/ipcDeleteStoreValues.ts index 019c6dab08..4dd7c5988c 100644 --- a/redisinsight/ui/src/electron/utils/ipcDeleteStoreValues.ts +++ b/redisinsight/ui/src/electron/utils/ipcDeleteStoreValues.ts @@ -1,5 +1,5 @@ import { ElectronStorageItem, IpcEvent } from 'uiSrc/electron/constants' export const ipcDeleteDownloadedVersion = async () => { - await window.electron.ipcRenderer.invoke(IpcEvent.deleteStoreValue, ElectronStorageItem.updateDownloadedVersion) + await window.app.ipc.invoke(IpcEvent.deleteStoreValue, ElectronStorageItem.updateDownloadedVersion) }