diff --git a/.circleci/config.yml b/.circleci/config.yml index f746df87e9..e273b4300f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1619,28 +1619,28 @@ workflows: - virustotal-url: name: Virus check - AppImage (nightly) - fileName: RedisInsight-v2-linux-x86_64.AppImage + fileName: RedisInsight-linux-x86_64.AppImage - virustotal-url: name: Virus check - deb (nightly) - fileName: RedisInsight-v2-linux-amd64.deb + fileName: RedisInsight-linux-amd64.deb - virustotal-url: name: Virus check - rpm (nightly) - fileName: RedisInsight-v2-linux-x86_64.rpm + fileName: RedisInsight-linux-x86_64.rpm - virustotal-url: name: Virus check - snap (nightly) - fileName: RedisInsight-v2-linux-amd64.snap + fileName: RedisInsight-linux-amd64.snap - virustotal-url: name: Virus check x64 - dmg (nightly) - fileName: RedisInsight-v2-mac-x64.dmg + fileName: RedisInsight-mac-x64.dmg - virustotal-url: name: Virus check arm64 - dmg (nightly) - fileName: RedisInsight-v2-mac-arm64.dmg + fileName: RedisInsight-mac-arm64.dmg - virustotal-url: name: Virus check MAS - pkg (nightly) fileName: RedisInsight-mac-universal-mas.pkg - virustotal-url: name: Virus check - exe (nightly) - fileName: RedisInsight-v2-win-installer.exe + fileName: RedisInsight-win-installer.exe - virustotal-report: name: Virus check report (prod) requires: diff --git a/.circleci/e2e/test.exe.cmd b/.circleci/e2e/test.exe.cmd index 417b54541c..052eeeccd7 100755 --- a/.circleci/e2e/test.exe.cmd +++ b/.circleci/e2e/test.exe.cmd @@ -1,7 +1,7 @@ @echo off set COMMON_URL=%USERPROFILE%/AppData/Local/Programs/redisinsight/resources/app.asar/dist/renderer/index.html -set ELECTRON_PATH=%USERPROFILE%/AppData/Local/Programs/redisinsight/RedisInsight-v2.exe +set ELECTRON_PATH=%USERPROFILE%/AppData/Local/Programs/redisinsight/RedisInsight.exe set OSS_STANDALONE_HOST=%E2E_CLOUD_DATABASE_HOST% set OSS_STANDALONE_PORT=%E2E_CLOUD_DATABASE_PORT% set OSS_STANDALONE_USERNAME=%E2E_CLOUD_DATABASE_USERNAME% diff --git a/.circleci/redisstack/app-image.repack.sh b/.circleci/redisstack/app-image.repack.sh index 7c5821d1b1..438c68c546 100755 --- a/.circleci/redisstack/app-image.repack.sh +++ b/.circleci/redisstack/app-image.repack.sh @@ -3,9 +3,9 @@ set -e ARCH=${ARCH:-x86_64} WORKING_DIRECTORY=$(pwd) -SOURCE_APP=${SOURCE_APP:-"RedisInsight-v2-linux-$ARCH.AppImage"} -APP_FOLDER_NAME="RedisInsight-v2-linux" -TAR_NAME="RedisInsight-v2-app-linux.$ARCH.tar.gz" +SOURCE_APP=${SOURCE_APP:-"RedisInsight-linux-$ARCH.AppImage"} +APP_FOLDER_NAME="RedisInsight-linux" +TAR_NAME="RedisInsight-app-linux.$ARCH.tar.gz" TMP_FOLDER="/tmp/RedisInsight-app-$ARCH" rm -rf "$TMP_FOLDER" diff --git a/.circleci/redisstack/build_modules.sh b/.circleci/redisstack/build_modules.sh index 5eb96b4c7e..a022db6d09 100755 --- a/.circleci/redisstack/build_modules.sh +++ b/.circleci/redisstack/build_modules.sh @@ -5,7 +5,7 @@ PLATFORM=${PLATFORM:-'linux'} ELECTRON_VERSION=$(cat electron/version) ARCH=${ARCH:-'x64'} #FILENAME="RedisInsight-$PLATFORM.$VERSION.$ARCH.zip" -FILENAME="RedisInsight-v2-web-$PLATFORM.$ARCH.tar.gz" +FILENAME="RedisInsight-web-$PLATFORM.$ARCH.tar.gz" # reinstall backend prod dependencies only (optimise space) rm -rf redisinsight/api/node_modules diff --git a/.circleci/redisstack/dmg.repack.sh b/.circleci/redisstack/dmg.repack.sh index 4b4aa12043..6b5f855876 100755 --- a/.circleci/redisstack/dmg.repack.sh +++ b/.circleci/redisstack/dmg.repack.sh @@ -3,8 +3,8 @@ set -e ARCH=${ARCH:-x64} WORKING_DIRECTORY=$(pwd) -TAR_NAME="RedisInsight-v2-app-darwin.$ARCH.tar.gz" -APP_FOLDER_NAME="RedisInsight-v2.app" +TAR_NAME="RedisInsight-app-darwin.$ARCH.tar.gz" +APP_FOLDER_NAME="RedisInsight.app" TMP_FOLDER="/tmp/$APP_FOLDER_NAME" rm -rf "$TMP_FOLDER" @@ -12,10 +12,10 @@ rm -rf "$TMP_FOLDER" mkdir -p "$WORKING_DIRECTORY/release/redisstack" mkdir -p "$TMP_FOLDER" -hdiutil attach "./release/RedisInsight-v2-mac-$ARCH.dmg" -cp -a /Volumes/RedisInsight-*/RedisInsight-v2.app "/tmp" +hdiutil attach "./release/RedisInsight-mac-$ARCH.dmg" +cp -a /Volumes/RedisInsight*/RedisInsight.app "/tmp" cd "/tmp" || exit 1 tar -czvf "$TAR_NAME" "$APP_FOLDER_NAME" cp "$TAR_NAME" "$WORKING_DIRECTORY/release/redisstack/" cd "$WORKING_DIRECTORY" || exit 1 -hdiutil unmount /Volumes/RedisInsight-*/ +hdiutil unmount /Volumes/RedisInsight*/ diff --git a/.gitignore b/.gitignore index 216f8b2fc2..3d2e62518d 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ licenses /tests/e2e/results /tests/e2e/remote /tests/e2e/.redisinsight-v2 +/tests/e2e/.redisinsight-app # Parcel .parcel-cache diff --git a/docs/plugins/installation.md b/docs/plugins/installation.md index 5192a6a173..ffaa3cec0a 100644 --- a/docs/plugins/installation.md +++ b/docs/plugins/installation.md @@ -9,9 +9,9 @@ authors to avoid automatic execution of malicious code. 1. Download the plugin for the Workbench. 2. Open the `plugins` folder with the following path - * For MacOs: `/.redisinsight-preview/plugins` - * For Windows: `C:/Users/{Username}/.redisinsight-preview/plugins` - * For Linux: `/.redisinsight-preview/plugins` + * For MacOs: `/.redisinsight-app/plugins` + * For Windows: `C:/Users/{Username}/.redisinsight-app/plugins` + * For Linux: `/.redisinsight-app/plugins` 3. Add the folder with plugin to the `plugins` folder To see the uploaded plugin visualizations in the command results, reload the Workbench diff --git a/electron-builder.json b/electron-builder.json index a9aa24d2f9..e376b17280 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -1,5 +1,5 @@ { - "productName": "RedisInsight-v2", + "productName": "RedisInsight", "appId": "org.RedisLabs.RedisInsight-V2", "copyright": "Copyright © 2023 Redis Ltd.", "files": [ diff --git a/jest.config.cjs b/jest.config.cjs index 24b0c9f37d..66ae5b8524 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -39,7 +39,6 @@ module.exports = { 'json', ], testEnvironment: 'jest-environment-jsdom', - // type: 'module', transformIgnorePatterns: [ 'node_modules/(?!(monaco-editor|react-monaco-editor)/)', ], @@ -61,8 +60,5 @@ module.exports = { functions: 72, lines: 80, }, - // './redisinsight/ui/src/slices/**/*.ts': { - // statements: 90, - // }, }, } diff --git a/package.json b/package.json index 28f62230ab..39e2e5bb9f 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "package:mac": "yarn build:prod && electron-builder build --mac -p never", "package:mac:arm": "yarn build:prod && electron-builder build --mac --arm64 -p never", "package:linux": "yarn build:prod && electron-builder build --linux -p never", - "postinstall": "skip-postinstall || (electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack.config.renderer.dev.dll.ts && opencollective-postinstall && yarn-deduplicate yarn.lock)", + "postinstall": "patch-package && skip-postinstall || (electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack.config.renderer.dev.dll.ts && opencollective-postinstall && yarn-deduplicate yarn.lock)", "start": "ts-node ./scripts/check-port-in-use.js && yarn start:renderer", "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./configs/webpack.config.renderer.dev.ts", "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack.config.preload.dev.ts", @@ -42,7 +42,7 @@ "start:web:public": "cross-env PUBLIC_DEV=true NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./configs/webpack.config.web.dev.ts", "test": "jest ./redisinsight/ui -w 1", "test:watch": "jest ./redisinsight/ui --watch -w 1", - "test:cov": "jest ./redisinsight/ui --coverage --no-cache --forceExit -w 3", + "test:cov": "jest ./redisinsight/ui --silent --coverage --no-cache --forceExit -w 3", "test:cov:unit": "jest ./redisinsight/ui --group=-component --coverage -w 1", "test:cov:component": "jest ./redisinsight/ui --group=component --coverage -w 1", "type-check:ui": "tsc --project redisinsight/ui --noEmit" @@ -204,6 +204,8 @@ "msw": "^1.3.2", "node-sass": "^8.0.0", "opencollective-postinstall": "^2.0.3", + "patch-package": "^8.0.0", + "postinstall-postinstall": "^2.1.0", "react-hot-loader": "^4.13.0", "react-refresh": "^0.9.0", "redux-mock-store": "^1.5.4", diff --git a/patches/react-vtree+3.0.0-beta.3.patch b/patches/react-vtree+3.0.0-beta.3.patch new file mode 100644 index 0000000000..cf1b00df37 --- /dev/null +++ b/patches/react-vtree+3.0.0-beta.3.patch @@ -0,0 +1,49 @@ +diff --git a/node_modules/react-vtree/dist/cjs/Tree.js b/node_modules/react-vtree/dist/cjs/Tree.js +index c46ce3e..879f0a6 100644 +--- a/node_modules/react-vtree/dist/cjs/Tree.js ++++ b/node_modules/react-vtree/dist/cjs/Tree.js +@@ -33,6 +33,7 @@ var Row = function Row(_ref) { + return /*#__PURE__*/_react.default.createElement(Node, Object.assign({ + isScrolling: isScrolling, + style: style, ++ index: index, + treeData: treeData + }, data)); + }; +diff --git a/node_modules/react-vtree/dist/es/Tree.d.ts b/node_modules/react-vtree/dist/es/Tree.d.ts +index 5e7f57e..b216b36 100644 +--- a/node_modules/react-vtree/dist/es/Tree.d.ts ++++ b/node_modules/react-vtree/dist/es/Tree.d.ts +@@ -24,6 +24,8 @@ export declare type NodePublicState = Readonly<{ + data: TData; + setOpen: (state: boolean) => Promise; + }> & { ++ index: number; ++ style: object; + isOpen: boolean; + }; + export declare type NodeRecord> = Readonly<{ +diff --git a/node_modules/react-vtree/dist/es/Tree.js b/node_modules/react-vtree/dist/es/Tree.js +index 2b1c7c0..b22e873 100644 +--- a/node_modules/react-vtree/dist/es/Tree.js ++++ b/node_modules/react-vtree/dist/es/Tree.js +@@ -19,6 +19,7 @@ export var Row = function Row(_ref) { + return /*#__PURE__*/React.createElement(Node, Object.assign({ + isScrolling: isScrolling, + style: style, ++ index: index, + treeData: treeData + }, data)); + }; +diff --git a/node_modules/react-vtree/dist/lib/Tree.js b/node_modules/react-vtree/dist/lib/Tree.js +index fb824bd..6feba4e 100644 +--- a/node_modules/react-vtree/dist/lib/Tree.js ++++ b/node_modules/react-vtree/dist/lib/Tree.js +@@ -17,6 +17,7 @@ export const Row = ({ + return /*#__PURE__*/React.createElement(Node, Object.assign({ + isScrolling: isScrolling, + style: style, ++ index: index, + treeData: treeData + }, data)); + }; diff --git a/redisinsight/api/config/default.ts b/redisinsight/api/config/default.ts index a7e39ef64b..10ba5e09c9 100644 --- a/redisinsight/api/config/default.ts +++ b/redisinsight/api/config/default.ts @@ -60,7 +60,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.36.0', + appVersion: process.env.APP_VERSION || '2.38.0', requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 25000, excludeRoutes: [], excludeAuthRoutes: [], @@ -77,7 +77,7 @@ export default { redis_clients: { idleSyncInterval: parseInt(process.env.CLIENTS_IDLE_SYNC_INTERVAL, 10) || 1000 * 60 * 60, // 1hr maxIdleThreshold: parseInt(process.env.CLIENTS_MAX_IDLE_THRESHOLD, 10) || 1000 * 60 * 60, // 1hr - retryTimes: parseInt(process.env.CLIENTS_RETRY_TIMES, 10) || 5, + retryTimes: parseInt(process.env.CLIENTS_RETRY_TIMES, 10) || 3, retryDelay: parseInt(process.env.CLIENTS_RETRY_DELAY, 10) || 500, maxRetriesPerRequest: parseInt(process.env.CLIENTS_MAX_RETRIES_PER_REQUEST, 10) || 1, }, diff --git a/redisinsight/api/config/production.ts b/redisinsight/api/config/production.ts index fbea805930..5eee260a68 100644 --- a/redisinsight/api/config/production.ts +++ b/redisinsight/api/config/production.ts @@ -2,9 +2,9 @@ import { join } from 'path'; import * as os from 'os'; const homedir = process.env.APP_FOLDER_ABSOLUTE_PATH - || (join(os.homedir(), process.env.APP_FOLDER_NAME || '.redisinsight-v2')); + || (join(os.homedir(), process.env.APP_FOLDER_NAME || '.redisinsight-app')); -const prevHomedir = join(os.homedir(), '.redisinsight-preview'); +const prevHomedir = join(os.homedir(), '.redisinsight-v2'); export default { dir_path: { diff --git a/redisinsight/api/config/staging.ts b/redisinsight/api/config/staging.ts index e28c0435ae..350ac23cab 100644 --- a/redisinsight/api/config/staging.ts +++ b/redisinsight/api/config/staging.ts @@ -2,9 +2,9 @@ import { join } from 'path'; import * as os from 'os'; const homedir = process.env.APP_FOLDER_ABSOLUTE_PATH - || (join(os.homedir(), process.env.APP_FOLDER_NAME || '.redisinsight-v2-stage')); + || (join(os.homedir(), process.env.APP_FOLDER_NAME || '.redisinsight-app-stage')); -const prevHomedir = join(os.homedir(), '.redisinsight-v2.0-stage'); +const prevHomedir = join(os.homedir(), '.redisinsight-v2-stage'); export default { dir_path: { diff --git a/redisinsight/api/config/swagger.ts b/redisinsight/api/config/swagger.ts index 22d24df01e..848273eb70 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.36.0', + version: '2.38.0', }, tags: [], }; diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index 633f3bb8e8..cab89a3019 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -1,6 +1,6 @@ { "name": "redisinsight-api", - "version": "2.36.0", + "version": "2.38.0", "description": "RedisInsight API", "private": true, "author": { @@ -42,7 +42,8 @@ "word-wrap": "1.2.4", "mocha/minimatch": "^3.0.5", "@nestjs/platform-socket.io/socket.io": "^4.7.1", - "**/semver": "^7.5.2" + "**/semver": "^7.5.2", + "winston-daily-rotate-file/**/file-stream-rotator": "^1.0.0" }, "dependencies": { "@nestjs/common": "^9.0.11", diff --git a/redisinsight/api/src/constants/error-messages.ts b/redisinsight/api/src/constants/error-messages.ts index 797ea2fc26..e87cf6357e 100644 --- a/redisinsight/api/src/constants/error-messages.ts +++ b/redisinsight/api/src/constants/error-messages.ts @@ -16,6 +16,8 @@ export default { CONNECTION_TIMEOUT: 'The connection has timed out, please check the connection details.', SERVER_CLOSED_CONNECTION: 'Server closed the connection.', + UNABLE_TO_ESTABLISH_CONNECTION: 'Unable to establish connection.', + RECONNECTING_TO_DATABASE: 'Reconnecting to the redis database.', AUTHENTICATION_FAILED: () => 'Failed to authenticate, please check the username or password.', INCORRECT_DATABASE_URL: (url) => `Could not connect to ${url}, please check the connection details.`, INCORRECT_CERTIFICATES: (url) => `Could not connect to ${url}, please check the CA or Client certificate.`, diff --git a/redisinsight/api/src/init-helper.ts b/redisinsight/api/src/init-helper.ts index f250f7e0a8..be8c67f4e1 100644 --- a/redisinsight/api/src/init-helper.ts +++ b/redisinsight/api/src/init-helper.ts @@ -28,6 +28,7 @@ export const migrateHomeFolder = async () => { await Promise.all([ 'redisinsight.db', 'plugins', + 'custom-tutorials', ].map((target) => copySource( join(PATH_CONFIG.prevHomedir, target), join(PATH_CONFIG.homedir, target), diff --git a/redisinsight/api/src/modules/analytics/analytics.service.spec.ts b/redisinsight/api/src/modules/analytics/analytics.service.spec.ts index 7c3241a3f5..18f6d9bf55 100644 --- a/redisinsight/api/src/modules/analytics/analytics.service.spec.ts +++ b/redisinsight/api/src/modules/analytics/analytics.service.spec.ts @@ -10,6 +10,7 @@ import { AppType } from 'src/modules/server/models/server'; import { SettingsService } from 'src/modules/settings/settings.service'; import { AnalyticsService, + Telemetry, NON_TRACKING_ANONYMOUS_ID, } from './analytics.service'; @@ -95,6 +96,11 @@ describe('AnalyticsService', () => { anonymousId: mockAnonymousId, integrations: { Amplitude: { session_id: sessionId } }, event: TelemetryEvents.ApplicationStarted, + context: { + traits: { + telemetry: Telemetry.Enabled, + }, + }, properties: { anonymousId: mockAnonymousId, buildType: AppType.Electron, @@ -128,6 +134,11 @@ describe('AnalyticsService', () => { anonymousId: NON_TRACKING_ANONYMOUS_ID, integrations: { Amplitude: { session_id: sessionId } }, event: TelemetryEvents.ApplicationStarted, + context: { + traits: { + telemetry: Telemetry.Disabled, + }, + }, properties: { anonymousId: mockAnonymousId, buildType: AppType.Electron, @@ -150,6 +161,11 @@ describe('AnalyticsService', () => { anonymousId: mockAnonymousId, integrations: { Amplitude: { session_id: sessionId } }, event: TelemetryEvents.ApplicationStarted, + context: { + traits: { + telemetry: Telemetry.Enabled, + }, + }, properties: { anonymousId: mockAnonymousId, buildType: AppType.Electron, @@ -186,6 +202,11 @@ describe('AnalyticsService', () => { anonymousId: mockAnonymousId, integrations: { Amplitude: { session_id: sessionId } }, name: TelemetryEvents.ApplicationStarted, + context: { + traits: { + telemetry: Telemetry.Enabled, + }, + }, properties: { anonymousId: mockAnonymousId, buildType: AppType.Electron, @@ -219,6 +240,11 @@ describe('AnalyticsService', () => { anonymousId: NON_TRACKING_ANONYMOUS_ID, integrations: { Amplitude: { session_id: sessionId } }, name: TelemetryEvents.ApplicationStarted, + context: { + traits: { + telemetry: Telemetry.Disabled, + }, + }, properties: { anonymousId: mockAnonymousId, buildType: AppType.Electron, @@ -241,6 +267,11 @@ describe('AnalyticsService', () => { anonymousId: mockAnonymousId, integrations: { Amplitude: { session_id: sessionId } }, name: TelemetryEvents.ApplicationStarted, + context: { + traits: { + telemetry: Telemetry.Enabled, + }, + }, properties: { anonymousId: mockAnonymousId, buildType: AppType.Electron, diff --git a/redisinsight/api/src/modules/analytics/analytics.service.ts b/redisinsight/api/src/modules/analytics/analytics.service.ts index 26b86ef0ee..fad204fb21 100644 --- a/redisinsight/api/src/modules/analytics/analytics.service.ts +++ b/redisinsight/api/src/modules/analytics/analytics.service.ts @@ -24,6 +24,11 @@ export interface ITelemetryInitEvent { appVersion: string; } +export enum Telemetry { + Enabled = 'enabled', + Disabled = 'disabled', +} + @Injectable() export class AnalyticsService { private anonymousId: string = NON_TRACKING_ANONYMOUS_ID; @@ -83,6 +88,11 @@ export class AnalyticsService { anonymousId: !isAnalyticsGranted && nonTracking ? NON_TRACKING_ANONYMOUS_ID : this.anonymousId, integrations: { Amplitude: { session_id: this.sessionId } }, event, + context: { + traits: { + telemetry: isAnalyticsGranted ? Telemetry.Enabled : Telemetry.Disabled, + } + }, properties: { ...eventData, anonymousId: this.anonymousId, @@ -116,6 +126,11 @@ export class AnalyticsService { name: event, anonymousId: !isAnalyticsGranted && nonTracking ? NON_TRACKING_ANONYMOUS_ID : this.anonymousId, integrations: { Amplitude: { session_id: this.sessionId } }, + context: { + traits: { + telemetry: isAnalyticsGranted ? Telemetry.Enabled : Telemetry.Disabled, + } + }, properties: { ...eventData, anonymousId: this.anonymousId, diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts index 5ccc7785b0..694c79d57c 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts @@ -101,6 +101,40 @@ describe('Standalone Scanner Strategy', () => { null, ); }); + it('should scan 2000 items when count provided more then 2k', async () => { + const args = { ...getKeysDto, count: 10_000, match: '*' }; + jest.spyOn(Utils, 'getTotal').mockResolvedValue(mockGetTotalResponse_1); + + when(browserTool.execCommand) + .calledWith( + mockBrowserClientMetadata, + BrowserToolKeysCommands.Scan, + expect.anything(), + null, + ) + .mockResolvedValue([0, [getKeyInfoResponse.name]]); + + strategy.getKeysInfo = jest.fn().mockResolvedValue([getKeyInfoResponse]); + + const result = await strategy.getKeys(mockBrowserClientMetadata, args); + + expect(result).toEqual([ + { + ...mockNodeEmptyResult, + total: 1, + scanned: 2000, + keys: [getKeyInfoResponse], + }, + ]); + expect(strategy.getKeysInfo).toHaveBeenCalled(); + expect(browserTool.execCommand).toHaveBeenNthCalledWith( + 1, + mockBrowserClientMetadata, + BrowserToolKeysCommands.Scan, + ['0', 'MATCH', args.match, 'COUNT', 2000], + null, + ); + }); it('should return keys names and type only', async () => { const args = { ...getKeysDto, type: 'string', match: 'pattern*', keysInfo: false, diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts index 79739907a5..ed98f915ac 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts @@ -86,6 +86,8 @@ export class StandaloneStrategy extends AbstractStrategy { count: number, type?: RedisDataType, ): Promise { + const COUNT = Math.min(2000, count); + let fullScanned = false; // todo: remove settings from here. threshold should be part of query? const settings = await this.settingsService.getAppSettings('1'); @@ -97,7 +99,7 @@ export class StandaloneStrategy extends AbstractStrategy { node.scanned < settings.scanThreshold ) ) { - let commandArgs = [`${node.cursor}`, 'MATCH', match, 'COUNT', count]; + let commandArgs = [`${node.cursor}`, 'MATCH', match, 'COUNT', COUNT]; if (type) { commandArgs = [...commandArgs, 'TYPE', type]; } @@ -112,7 +114,7 @@ export class StandaloneStrategy extends AbstractStrategy { // eslint-disable-next-line no-param-reassign node.cursor = parseInt(nextCursor, 10); // eslint-disable-next-line no-param-reassign - node.scanned += count; + node.scanned += COUNT; node.keys.push(...keys); fullScanned = node.cursor === 0; } diff --git a/redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.fs.provider.spec.ts b/redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.fs.provider.spec.ts index 99ffb54564..147bcd9931 100644 --- a/redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.fs.provider.spec.ts +++ b/redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.fs.provider.spec.ts @@ -74,7 +74,7 @@ describe('CustomTutorialFsProvider', () => { prepareTmpFolderSpy.mockRestore(); const result = await service.unzipFromMemoryStoredFile(mockCustomTutorialZipFile); - expect(result).toContain(`${PATH_CONFIG.tmpDir}/RedisInsight-v2/custom-tutorials`); + expect(result).toContain(`${PATH_CONFIG.tmpDir}/RedisInsight/custom-tutorials`); expect(mFs.copy).toHaveBeenCalled(); }); }); diff --git a/redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.fs.provider.ts b/redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.fs.provider.ts index 4196c774fd..010f72c367 100644 --- a/redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.fs.provider.ts +++ b/redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.fs.provider.ts @@ -11,7 +11,7 @@ import ERROR_MESSAGES from 'src/constants/error-messages'; const PATH_CONFIG = config.get('dir_path'); -const TMP_FOLDER = `${PATH_CONFIG.tmpDir}/RedisInsight-v2/custom-tutorials`; +const TMP_FOLDER = `${PATH_CONFIG.tmpDir}/RedisInsight/custom-tutorials`; @Injectable() export class CustomTutorialFsProvider { diff --git a/redisinsight/api/src/modules/database/providers/database.factory.ts b/redisinsight/api/src/modules/database/providers/database.factory.ts index 82d07fffa9..ec32583f58 100644 --- a/redisinsight/api/src/modules/database/providers/database.factory.ts +++ b/redisinsight/api/src/modules/database/providers/database.factory.ts @@ -38,7 +38,7 @@ export class DatabaseFactory { context: ClientContext.Common, }, database, - { useRetry: false }, + { useRetry: true }, ); if (await this.databaseInfoProvider.isSentinel(client)) { @@ -109,7 +109,7 @@ export class DatabaseFactory { context: ClientContext.Common, }, model, - { useRetry: false }, + { useRetry: true }, ); // todo: rethink @@ -157,7 +157,7 @@ export class DatabaseFactory { context: ClientContext.Common, }, model, - { useRetry: false }, + { useRetry: true }, ); model.connectionType = ConnectionType.SENTINEL; diff --git a/redisinsight/api/src/modules/redis/redis-connection.factory.spec.ts b/redisinsight/api/src/modules/redis/redis-connection.factory.spec.ts index 18cc849767..6cf370dca5 100644 --- a/redisinsight/api/src/modules/redis/redis-connection.factory.spec.ts +++ b/redisinsight/api/src/modules/redis/redis-connection.factory.spec.ts @@ -87,11 +87,21 @@ describe('RedisConnectionFactory', () => { process.nextTick(() => mockClient.emit('ready')); }); - it('should fail to create standalone connection', (done) => { + it('should successfully create standalone client even with error event emited', (done) => { + service.createStandaloneConnection(mockClientMetadata, mockDatabaseWithTlsAuth, { useRetry: true }) + .then(checkClient(done, mockClient)); + process.nextTick(() => mockClient.emit('error', mockError)); + process.nextTick(() => mockClient.emit('ready')); + }); + + it('should fail to create standalone with last error', (done) => { service.createStandaloneConnection(mockClientMetadata, mockDatabaseWithTlsAuth, {}) .catch(checkError(done)); + process.nextTick(() => mockClient.emit('error', new Error('1'))); + process.nextTick(() => mockClient.emit('error', new Error('2'))); process.nextTick(() => mockClient.emit('error', mockError)); + process.nextTick(() => mockClient.emit('end')); }); it('should handle sync error during standalone client creation', (done) => { @@ -113,11 +123,22 @@ describe('RedisConnectionFactory', () => { process.nextTick(() => mockCluster.emit('ready')); }); - it('should fail to create cluster connection', (done) => { + it('should successfully create cluster client and not fail even when error emited', (done) => { + service.createClusterConnection(mockClientMetadata, mockClusterDatabaseWithTlsAuth, {}) + .then(checkClient(done, mockCluster)); + + process.nextTick(() => mockCluster.emit('error', mockError)); + process.nextTick(() => mockCluster.emit('ready')); + }); + + it('should fail to create cluster connection with last error', (done) => { service.createClusterConnection(mockClientMetadata, mockClusterDatabaseWithTlsAuth, {}) .catch(checkError(done)); + process.nextTick(() => mockCluster.emit('error', new Error('1'))); + process.nextTick(() => mockCluster.emit('error', new Error('2'))); process.nextTick(() => mockCluster.emit('error', mockError)); + process.nextTick(() => mockCluster.emit('end')); }); it('should handle sync error during cluster client creation', (done) => { @@ -138,11 +159,22 @@ describe('RedisConnectionFactory', () => { process.nextTick(() => mockClient.emit('ready')); }); - it('should fail to create sentinel connection', (done) => { + it('should successfully create sentinel client and not fail even when error emited', (done) => { + service.createSentinelConnection(mockClientMetadata, mockSentinelDatabaseWithTlsAuth, { useRetry: true }) + .then(checkClient(done, mockClient)); + + process.nextTick(() => mockClient.emit('error', mockError)); + process.nextTick(() => mockClient.emit('ready')); + }); + + it('should fail to create sentinel connection with last error', (done) => { service.createSentinelConnection(mockClientMetadata, mockSentinelDatabaseWithTlsAuth, {}) .catch(checkError(done)); + process.nextTick(() => mockClient.emit('error', new Error('1'))); + process.nextTick(() => mockClient.emit('error', new Error('2'))); process.nextTick(() => mockClient.emit('error', mockError)); + process.nextTick(() => mockClient.emit('end')); }); it('should handle sync error during sentinel client creation', (done) => { diff --git a/redisinsight/api/src/modules/redis/redis-connection.factory.ts b/redisinsight/api/src/modules/redis/redis-connection.factory.ts index 014275b6a9..c57a2e1078 100644 --- a/redisinsight/api/src/modules/redis/redis-connection.factory.ts +++ b/redisinsight/api/src/modules/redis/redis-connection.factory.ts @@ -3,7 +3,7 @@ import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common import { Database } from 'src/modules/database/models/database'; import apiConfig from 'src/utils/config'; import { ConnectionOptions } from 'tls'; -import { isEmpty, isNumber } from 'lodash'; +import { isEmpty, isNumber, set } from 'lodash'; import { cloneClassInstance, generateRedisConnectionName } from 'src/utils'; import { ConnectionType } from 'src/modules/database/entities/database.entity'; import { ClientMetadata } from 'src/common/models'; @@ -164,9 +164,12 @@ export class RedisConnectionFactory { options: IRedisConnectionOptions, ): Promise { let tnl; + let connection: Redis; try { const config = await this.getRedisOptions(clientMetadata, database, options); + // cover cases when we are connecting to sentinel using standalone client to discover master groups + const dbIndex = config.db > 0 && !database.sentinelMaster ? config.db : 0; if (database.ssh) { tnl = await this.sshTunnelProvider.createTunnel(database); @@ -174,6 +177,8 @@ export class RedisConnectionFactory { return await new Promise((resolve, reject) => { try { + let lastError: Error; + if (tnl) { tnl.on('error', (error) => { reject(error); @@ -187,31 +192,45 @@ export class RedisConnectionFactory { config.port = tnl.serverAddress.port; } - const connection = new Redis({ + connection = new Redis({ ...config, - // cover cases when we are connecting to sentinel as to standalone to discover master groups - db: config.db > 0 && !database.sentinelMaster ? config.db : 0, + db: 0, }); connection.on('error', (e): void => { this.logger.error('Failed connection to the redis database.', e); - reject(e); + lastError = e; }); connection.on('end', (): void => { - this.logger.error(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION); - reject(new InternalServerErrorException(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION)); + this.logger.error(ERROR_MESSAGES.UNABLE_TO_ESTABLISH_CONNECTION, lastError); + reject(lastError || new InternalServerErrorException(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION)); }); connection.on('ready', (): void => { + lastError = null; this.logger.log('Successfully connected to the redis database'); - resolve(connection); + + // manual switch to particular logical db + // since ioredis doesn't handle "select" command error during connection + if (dbIndex > 0) { + connection.select(dbIndex) + .then(() => { + set(connection, ['options', 'db'], dbIndex); + resolve(connection); + }) + .catch(reject); + } else { + resolve(connection); + } }); connection.on('reconnecting', (): void => { - this.logger.log('Reconnecting to the redis database'); + lastError = null; + this.logger.log(ERROR_MESSAGES.RECONNECTING_TO_DATABASE); }); } catch (e) { reject(e); } }) as Redis; } catch (e) { + connection?.disconnect?.(); tnl?.close?.(); throw e; } @@ -228,36 +247,66 @@ export class RedisConnectionFactory { database: Database, options: IRedisConnectionOptions, ): Promise { - const config = await this.getRedisClusterOptions(clientMetadata, database, options); + let connection: Cluster; - if (database.ssh) { - throw new Error('SSH is unsupported for cluster databases.'); - } + try { + const config = await this.getRedisClusterOptions(clientMetadata, database, options); - return new Promise((resolve, reject) => { - try { - const cluster = new Cluster([{ - host: database.host, - port: database.port, - }].concat(database.nodes), { - ...config, - }); - cluster.on('error', (e): void => { - this.logger.error('Failed connection to the redis oss cluster', e); - reject(!isEmpty(e.lastNodeError) ? e.lastNodeError : e); - }); - cluster.on('end', (): void => { - this.logger.error(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION); - reject(new InternalServerErrorException(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION)); - }); - cluster.on('ready', (): void => { - this.logger.log('Successfully connected to the redis oss cluster.'); - resolve(cluster); - }); - } catch (e) { - reject(e); + if (database.ssh) { + throw new Error('SSH is unsupported for cluster databases.'); } - }); + + return await (new Promise((resolve, reject) => { + try { + let lastError: Error; + + connection = new Cluster([{ + host: database.host, + port: database.port, + }].concat(database.nodes), { + ...config, + redisOptions: { + ...config.redisOptions, + db: 0, + }, + }); + connection.on('error', (e): void => { + this.logger.error('Failed connection to the redis oss cluster', e); + lastError = !isEmpty(e.lastNodeError) ? e.lastNodeError : e; + }); + connection.on('end', (): void => { + this.logger.error(ERROR_MESSAGES.UNABLE_TO_ESTABLISH_CONNECTION, lastError); + reject(lastError || new InternalServerErrorException(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION)); + }); + connection.on('ready', (): void => { + lastError = null; + this.logger.log('Successfully connected to the redis oss cluster.'); + + // manual switch to particular logical db + // since ioredis doesn't handle "select" command error during connection + if (config.redisOptions.db > 0) { + connection.select(config.redisOptions.db) + .then(() => { + set(connection, ['options', 'db'], config.redisOptions.db); + resolve(connection); + }) + .catch(reject); + } else { + resolve(connection); + } + }); + connection.on('reconnecting', (): void => { + lastError = null; + this.logger.log(ERROR_MESSAGES.RECONNECTING_TO_DATABASE); + }); + } catch (e) { + reject(e); + } + })); + } catch (e) { + connection?.disconnect?.(); + throw e; + } } /** @@ -271,27 +320,56 @@ export class RedisConnectionFactory { database: Database, options: IRedisConnectionOptions, ): Promise { - const config = await this.getRedisSentinelOptions(clientMetadata, database, options); + let connection: Redis; - return new Promise((resolve, reject) => { - try { - const client = new Redis(config); - client.on('error', (e): void => { - this.logger.error('Failed connection to the redis oss sentinel', e); + try { + const config = await this.getRedisSentinelOptions(clientMetadata, database, options); + + return await (new Promise((resolve, reject) => { + try { + let lastError: Error; + + connection = new Redis({ + ...config, + db: 0, + }); + connection.on('error', (e): void => { + this.logger.error('Failed connection to the redis oss sentinel', e); + lastError = e; + }); + connection.on('end', (): void => { + this.logger.error(ERROR_MESSAGES.UNABLE_TO_ESTABLISH_CONNECTION, lastError); + reject(lastError || new InternalServerErrorException(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION)); + }); + connection.on('ready', (): void => { + lastError = null; + this.logger.log('Successfully connected to the redis oss sentinel.'); + + // manual switch to particular logical db + // since ioredis doesn't handle "select" command error during connection + if (config.db > 0) { + connection.select(config.db) + .then(() => { + set(connection, ['options', 'db'], config.db); + resolve(connection); + }) + .catch(reject); + } else { + resolve(connection); + } + }); + connection.on('reconnecting', (): void => { + lastError = null; + this.logger.log(ERROR_MESSAGES.RECONNECTING_TO_DATABASE); + }); + } catch (e) { reject(e); - }); - client.on('end', (): void => { - this.logger.error(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION); - reject(new InternalServerErrorException(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION)); - }); - client.on('ready', (): void => { - this.logger.log('Successfully connected to the redis oss sentinel.'); - resolve(client); - }); - } catch (e) { - reject(e); - } - }); + } + })); + } catch (e) { + connection?.disconnect?.(); + throw e; + } } /** diff --git a/redisinsight/api/yarn.lock b/redisinsight/api/yarn.lock index 2c28d8aed2..252ab1e1c7 100644 --- a/redisinsight/api/yarn.lock +++ b/redisinsight/api/yarn.lock @@ -3919,12 +3919,10 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-stream-rotator@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz#007019e735b262bb6c6f0197e58e5c87cb96cec3" - integrity sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ== - dependencies: - moment "^2.29.1" +file-stream-rotator@^0.6.1, file-stream-rotator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-stream-rotator/-/file-stream-rotator-1.0.0.tgz#de58379321a1ea6d2938ed5f5a2eff3b7f8b2780" + integrity sha512-qg5mQO7o+vhS7NPqkrkfJS8qqhz0d17Tnewmb5sUTUKwYe27LKaDtbTuRAtQWkBn6jROuFPVIDF5DtckzokFTQ== file-type@^16.5.4: version "16.5.4" @@ -6201,11 +6199,6 @@ mocha@^8.4.0: yargs-parser "20.2.4" yargs-unparser "2.0.0" -moment@^2.29.1: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" diff --git a/redisinsight/desktop/app.ts b/redisinsight/desktop/app.ts index 46a4984658..73721edf37 100644 --- a/redisinsight/desktop/app.ts +++ b/redisinsight/desktop/app.ts @@ -1,5 +1,6 @@ /* eslint global-require: off, no-console: off */ import { app, nativeTheme } from 'electron' +import log from 'electron-log' import path from 'path' import { initElectronHandlers, @@ -7,7 +8,7 @@ import { WindowType, windowFactory, AboutPanelOptions, - checkForUpdate, + initAutoUpdateChecks, installExtensions, initTray, initAutoUpdaterHandlers, @@ -70,10 +71,12 @@ const init = async () => { await windowFactory(WindowType.Main, splashWindow, { parsedDeepLink }) - checkForUpdate(process.env.MANUAL_UPGRADES_LINK || process.env.UPGRADES_LINK) - } catch (_err) { - const error = _err as Error - console.log(wrapErrorMessageSensitiveData(error)) + initAutoUpdateChecks( + process.env.MANUAL_UPGRADES_LINK || process.env.UPGRADES_LINK, + parseInt(process.env.RIAUTOUPDATEINTERVAL, 10) || 84 * 3600 * 1000, + ) + } catch (err) { + log.error(wrapErrorMessageSensitiveData(err as Error)) } } diff --git a/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts b/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts index 3924b20cb5..392158b196 100644 --- a/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts +++ b/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts @@ -7,8 +7,8 @@ const ICON_PATH = app.isPackaged : path.join(__dirname, '../resources', 'icon.png') export const AboutPanelOptions = { - applicationName: 'RedisInsight-v2', - applicationVersion: `${app.getVersion() || '2.36.0'}${ + applicationName: 'RedisInsight', + applicationVersion: `${app.getVersion() || '2.38.0'}${ !config.isProduction ? `-dev-${process.getCreationTime()}` : '' }`, copyright: `Copyright © ${new Date().getFullYear()} Redis Ltd.`, diff --git a/redisinsight/desktop/src/lib/updater/updater.ts b/redisinsight/desktop/src/lib/updater/updater.ts index 9ac8f44c72..063c8ff96c 100644 --- a/redisinsight/desktop/src/lib/updater/updater.ts +++ b/redisinsight/desktop/src/lib/updater/updater.ts @@ -33,7 +33,19 @@ export const checkForUpdate = async (url: string = '') => { autoUpdater.autoDownload = true autoUpdater.autoInstallOnAppQuit = true - await autoUpdater.checkForUpdates() + const res = await autoUpdater.checkForUpdates() + + if (res?.downloadPromise) { + await res.downloadPromise + } +} + +export const initAutoUpdateChecks = (url = '', interval = 84 * 3600 * 1000) => { + checkForUpdate(url) + .catch((e) => log.error(wrapErrorMessageSensitiveData(e))) + .finally(() => { + setTimeout(() => initAutoUpdateChecks(url, interval), interval) + }) } export const quitAndInstallUpdate = () => { diff --git a/redisinsight/package.json b/redisinsight/package.json index 2ec250a74f..b595f5ceed 100644 --- a/redisinsight/package.json +++ b/redisinsight/package.json @@ -2,7 +2,7 @@ "name": "redisinsight", "productName": "RedisInsight", "private": true, - "version": "2.36.0", + "version": "2.38.0", "description": "RedisInsight", "main": "./dist/main/main.js", "author": { diff --git a/redisinsight/ui/src/assets/img/browser/treeViewSort.svg b/redisinsight/ui/src/assets/img/browser/treeViewSort.svg new file mode 100644 index 0000000000..97b88a16ed --- /dev/null +++ b/redisinsight/ui/src/assets/img/browser/treeViewSort.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.spec.tsx b/redisinsight/ui/src/components/auto-refresh/AutoRefresh.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.spec.tsx rename to redisinsight/ui/src/components/auto-refresh/AutoRefresh.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx b/redisinsight/ui/src/components/auto-refresh/AutoRefresh.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx rename to redisinsight/ui/src/components/auto-refresh/AutoRefresh.tsx diff --git a/redisinsight/ui/src/pages/browser/components/auto-refresh/index.ts b/redisinsight/ui/src/components/auto-refresh/index.ts similarity index 100% rename from redisinsight/ui/src/pages/browser/components/auto-refresh/index.ts rename to redisinsight/ui/src/components/auto-refresh/index.ts diff --git a/redisinsight/ui/src/pages/browser/components/auto-refresh/styles.module.scss b/redisinsight/ui/src/components/auto-refresh/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/auto-refresh/styles.module.scss rename to redisinsight/ui/src/components/auto-refresh/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/auto-refresh/utils.ts b/redisinsight/ui/src/components/auto-refresh/utils.ts similarity index 100% rename from redisinsight/ui/src/pages/browser/components/auto-refresh/utils.ts rename to redisinsight/ui/src/components/auto-refresh/utils.ts diff --git a/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.spec.tsx b/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.spec.tsx index 2d7742da38..5b3edaf369 100644 --- a/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.spec.tsx +++ b/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.spec.tsx @@ -4,13 +4,13 @@ import React from 'react' import MockedSocket from 'socket.io-mock' import socketIO from 'socket.io-client' import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils' -import { BulkActionsServerEvent, BulkActionsType, SocketEvent } from 'uiSrc/constants' +import { BulkActionsServerEvent, BulkActionsStatus, BulkActionsType, SocketEvent } from 'uiSrc/constants' import { bulkActionsDeleteSelector, bulkActionsSelector, disconnectBulkDeleteAction, setBulkActionConnected, - setBulkDeleteLoading + setBulkDeleteLoading, setDeleteOverviewStatus } from 'uiSrc/slices/browser/bulkActions' import BulkActionsConfig from './BulkActionsConfig' @@ -110,6 +110,7 @@ describe('BulkActionsConfig', () => { const afterRenderActions = [ setBulkActionConnected(true), setBulkDeleteLoading(true), + setDeleteOverviewStatus(BulkActionsStatus.Disconnected), disconnectBulkDeleteAction(), ] expect(store.getActions()).toEqual([...afterRenderActions]) diff --git a/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.tsx b/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.tsx index 410bb5458a..772fb2f377 100644 --- a/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.tsx +++ b/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.tsx @@ -11,6 +11,7 @@ import { setDeleteOverview, setBulkActionsInitialState, bulkActionsDeleteSelector, + setDeleteOverviewStatus, } from 'uiSrc/slices/browser/bulkActions' import { getBaseApiUrl, Nullable } from 'uiSrc/utils' import { sessionStorageService } from 'uiSrc/services' @@ -21,11 +22,7 @@ import { BrowserStorageItem, BulkActionsServerEvent, BulkActionsStatus, BulkActi import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { CustomHeaders } from 'uiSrc/constants/api' -interface IProps { - retryDelay?: number -} - -const BulkActionsConfig = ({ retryDelay = 5000 } : IProps) => { +const BulkActionsConfig = () => { const { id: instanceId = '', db } = useSelector(connectedInstanceSelector) const { isConnected } = useSelector(bulkActionsSelector) const { isActionTriggered: isDeleteTriggered } = useSelector(bulkActionsDeleteSelector) @@ -60,11 +57,8 @@ const BulkActionsConfig = ({ retryDelay = 5000 } : IProps) => { // Catch disconnect socketRef.current?.on(SocketEvent.Disconnect, () => { - if (retryDelay) { - retryTimer = setTimeout(handleDisconnect, retryDelay) - } else { - handleDisconnect() - } + dispatch(setDeleteOverviewStatus(BulkActionsStatus.Disconnected)) + handleDisconnect() }) }, [instanceId, isDeleteTriggered]) @@ -147,10 +141,8 @@ const BulkActionsConfig = ({ retryDelay = 5000 } : IProps) => { const onBulkDeleteAborted = (data: any) => { dispatch(setBulkDeleteLoading(false)) sessionStorageService.set(BrowserStorageItem.bulkActionDeleteId, '') - - if (data.status === 'aborted') { - dispatch(setDeleteOverview(data)) - } + dispatch(setDeleteOverview(data)) + handleDisconnect() } useEffect(() => { diff --git a/redisinsight/ui/src/components/full-screen/FullScreen.spec.tsx b/redisinsight/ui/src/components/full-screen/FullScreen.spec.tsx new file mode 100644 index 0000000000..28f83b5495 --- /dev/null +++ b/redisinsight/ui/src/components/full-screen/FullScreen.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, FullScreen } from './FullScreen' + +const mockedProps = mock() + +describe('FullScreen', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/components/full-screen/FullScreen.tsx b/redisinsight/ui/src/components/full-screen/FullScreen.tsx new file mode 100644 index 0000000000..e2dd13a3f0 --- /dev/null +++ b/redisinsight/ui/src/components/full-screen/FullScreen.tsx @@ -0,0 +1,35 @@ +import { + EuiButtonIcon, + EuiToolTip, +} from '@elastic/eui' +import React from 'react' + +export interface Props { + isFullScreen: boolean + onToggleFullScreen: () => void + anchorClassName?: string + btnTestId?: string +} + +const FullScreen = ({ + isFullScreen, + onToggleFullScreen, + anchorClassName = '', + btnTestId = 'toggle-full-screen', +}: Props) => ( + + + +) + +export { FullScreen } diff --git a/redisinsight/ui/src/components/full-screen/index.ts b/redisinsight/ui/src/components/full-screen/index.ts new file mode 100644 index 0000000000..82cb205613 --- /dev/null +++ b/redisinsight/ui/src/components/full-screen/index.ts @@ -0,0 +1 @@ +export { FullScreen } from './FullScreen' diff --git a/redisinsight/ui/src/components/index.ts b/redisinsight/ui/src/components/index.ts index bdf340c3a2..f16759412b 100644 --- a/redisinsight/ui/src/components/index.ts +++ b/redisinsight/ui/src/components/index.ts @@ -26,8 +26,11 @@ import ShowChildByCondition from './show-child-by-condition' import RecommendationVoting from './recommendation-voting' import RecommendationCopyComponent from './recommendation-copy-component' import FeatureFlagComponent from './feature-flag-component' +import AutoRefresh from './auto-refresh' import { ModuleNotLoaded, FilterNotAvailable } from './messages' +export { FullScreen } from './full-screen' + export * from './oauth' export { @@ -64,4 +67,5 @@ export { FeatureFlagComponent, ModuleNotLoaded, FilterNotAvailable, + AutoRefresh, } diff --git a/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx b/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx index aea5e73c2d..979c19b881 100644 --- a/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx +++ b/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx @@ -2,10 +2,14 @@ import React from 'react' import cx from 'classnames' import { isNull } from 'lodash' import { EuiText, EuiTextColor } from '@elastic/eui' +import { useSelector } from 'react-redux' import { numberWithSpaces, nullableNumberWithSpaces } from 'uiSrc/utils/numbers' -import ScanMore from '../scan-more' +import { KeyViewType } from 'uiSrc/slices/interfaces/keys' +import { keysSelector } from 'uiSrc/slices/browser/keys' +import { KeyTreeSettings } from 'uiSrc/pages/browser/components/key-tree' +import ScanMore from '../scan-more' import styles from './styles.module.scss' export interface Props { @@ -41,6 +45,8 @@ const KeysSummary = (props: Props) => { && nextCursor !== '0' ? '~' : '' + const { viewType } = useSelector(keysSelector) + return ( <> {(!!totalItemsCount || isNull(totalItemsCount)) && ( @@ -88,6 +94,9 @@ const KeysSummary = (props: Props) => { )} + {viewType === KeyViewType.Tree && ( + + )} )} {loading && !totalItemsCount && !isNull(totalItemsCount) && ( diff --git a/redisinsight/ui/src/components/keys-summary/styles.module.scss b/redisinsight/ui/src/components/keys-summary/styles.module.scss index 198f838ff6..294a787183 100644 --- a/redisinsight/ui/src/components/keys-summary/styles.module.scss +++ b/redisinsight/ui/src/components/keys-summary/styles.module.scss @@ -1,3 +1,7 @@ +.content { + display: flex; +} + .loading { opacity: 0; } 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 919ccfbc46..a81e212f7f 100644 --- a/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx +++ b/redisinsight/ui/src/components/messages/filter-not-available/FilterNotAvailable.tsx @@ -28,7 +28,7 @@ const FilterNotAvailable = ({ onClose } : { onClose?: () => void }) => { {!!freeInstance && ( <> - Use your free all-in-one Redis Enterprise Cloud database to start exploring these capabilities. + Use your free all-in-one Redis Cloud database to start exploring these capabilities. 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 70e610327c..ef0cd8993b 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx @@ -84,7 +84,7 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps ) : ( - Use your free all-in-one Redis Enterprise Cloud database to start exploring these capabilities. + Use your free all-in-one Redis Cloud database to start exploring these capabilities. )), [freeInstance]) diff --git a/redisinsight/ui/src/components/monitor-config/MonitorConfig.spec.tsx b/redisinsight/ui/src/components/monitor-config/MonitorConfig.spec.tsx index 5d0f8cc246..0b4e0fcfd6 100644 --- a/redisinsight/ui/src/components/monitor-config/MonitorConfig.spec.tsx +++ b/redisinsight/ui/src/components/monitor-config/MonitorConfig.spec.tsx @@ -9,7 +9,7 @@ import { pauseMonitor, setSocket, stopMonitor, - lockResume + lockResume, setLogFileId, setStartTimestamp } from 'uiSrc/slices/cli/monitor' import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils' import { MonitorEvent, SocketEvent } from 'uiSrc/constants' @@ -69,26 +69,47 @@ describe('MonitorConfig', () => { it(`should emit ${MonitorEvent.Monitor} event`, () => { const monitorSelectorMock = jest.fn().mockReturnValue({ isRunning: true, + isSaveToFile: true }) monitorSelector.mockImplementation(monitorSelectorMock) const { unmount } = render() - socket.on(MonitorEvent.MonitorData, (data: []) => { - expect(data).toEqual(['message1', 'message2']) + socket.socketClient.on(MonitorEvent.Monitor, (data: any) => { + expect(data).toEqual({ logFileId: expect.any(String) }) }) - socket.socketClient.emit(MonitorEvent.MonitorData, ['message1', 'message2']) + socket.socketClient.emit(SocketEvent.Connect) const afterRenderActions = [ setSocket(socket), - setMonitorLoadingPause(true) + setMonitorLoadingPause(true), + setLogFileId(expect.any(String)), + setStartTimestamp(expect.any(Number)) ] expect(store.getActions()).toEqual([...afterRenderActions]) unmount() }) + it(`should not emit ${MonitorEvent.Monitor} event when paused`, () => { + const monitorSelectorMock = jest.fn().mockReturnValue({ + isRunning: true, + isPaused: true + }) + monitorSelector.mockImplementation(monitorSelectorMock) + + const { unmount } = render() + const mockedMonitorEvent = jest.fn() + + socket.socketClient.on(MonitorEvent.Monitor, mockedMonitorEvent) + socket.socketClient.emit(SocketEvent.Connect) + + expect(mockedMonitorEvent).not.toBeCalled() + + unmount() + }) + it('monitor should catch Exception', () => { const { unmount } = render() diff --git a/redisinsight/ui/src/components/monitor-config/MonitorConfig.tsx b/redisinsight/ui/src/components/monitor-config/MonitorConfig.tsx index f61fc6ad21..8649203560 100644 --- a/redisinsight/ui/src/components/monitor-config/MonitorConfig.tsx +++ b/redisinsight/ui/src/components/monitor-config/MonitorConfig.tsx @@ -1,7 +1,7 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' import { debounce } from 'lodash' -import { io } from 'socket.io-client' +import { io, Socket } from 'socket.io-client' import { v4 as uuidv4 } from 'uuid' import { @@ -16,7 +16,7 @@ import { setLogFileId, pauseMonitor, lockResume } from 'uiSrc/slices/cli/monitor' -import { getBaseApiUrl } from 'uiSrc/utils' +import { getBaseApiUrl, Nullable } from 'uiSrc/utils' import { MonitorErrorMessages, MonitorEvent, SocketErrors, SocketEvent } from 'uiSrc/constants' import { IMonitorDataPayload } from 'uiSrc/slices/interfaces' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' @@ -26,12 +26,18 @@ import { IMonitorData } from 'apiSrc/modules/profiler/interfaces/monitor-data.in import ApiStatusCode from '../../constants/apiStatusCode' interface IProps { - retryDelay?: number; + retryDelay?: number } const MonitorConfig = ({ retryDelay = 15000 } : IProps) => { const { id: instanceId = '' } = useSelector(connectedInstanceSelector) const { socket, isRunning, isPaused, isSaveToFile, isMinimizedMonitor, isShowMonitor } = useSelector(monitorSelector) + const socketRef = useRef>(null) + const logFileIdRef = useRef() + const timestampRef = useRef() + const retryTimerRef = useRef() + const payloadsRef = useRef([]) + const dispatch = useDispatch() const setNewItems = debounce((items, onSuccess?) => { @@ -52,56 +58,28 @@ const MonitorConfig = ({ retryDelay = 15000 } : IProps) => { if (!isRunning || !instanceId || socket?.connected) { return } - const logFileId = `_redis_${uuidv4()}` - const timestamp = Date.now() - let retryTimer: NodeJS.Timer + + logFileIdRef.current = `_redis_${uuidv4()}` + timestampRef.current = Date.now() // Create SocketIO connection to instance by instanceId - const newSocket = io(`${getBaseApiUrl()}/monitor`, { + socketRef.current = io(`${getBaseApiUrl()}/monitor`, { forceNew: true, query: { instanceId }, extraHeaders: { [CustomHeaders.WindowId]: window.windowId || '' }, rejectUnauthorized: false, }) - dispatch(setSocket(newSocket)) - let payloads: IMonitorDataPayload[] = [] - - const handleMonitorEvents = () => { - dispatch(setMonitorLoadingPause(false)) - newSocket.on(MonitorEvent.MonitorData, (payload: IMonitorData[]) => { - payloads = payloads.concat(payload) - - // set batch of payloads and then clear batch - setNewItems(payloads, () => { - payloads.length = 0 - // reset all timings after items were changed - setNewItems.cancel() - }) - }) - } + dispatch(setSocket(socketRef.current)) const handleDisconnect = () => { - newSocket.removeAllListeners() + socketRef.current?.removeAllListeners() dispatch(pauseMonitor()) dispatch(stopMonitor()) dispatch(lockResume()) } - newSocket.on(SocketEvent.Connect, () => { - // Trigger Monitor event - clearTimeout(retryTimer) - - dispatch(setLogFileId(logFileId)) - dispatch(setStartTimestamp(timestamp)) - newSocket.emit( - MonitorEvent.Monitor, - { logFileId: isSaveToFile ? logFileId : null }, - handleMonitorEvents - ) - }) - // Catch exceptions - newSocket.on(MonitorEvent.Exception, (payload) => { + socketRef.current?.on(MonitorEvent.Exception, (payload) => { if (payload.status === ApiStatusCode.Forbidden) { handleDisconnect() dispatch(setError(MonitorErrorMessages.NoPerm)) @@ -109,26 +87,51 @@ const MonitorConfig = ({ retryDelay = 15000 } : IProps) => { return } - payloads.push({ isError: true, time: `${Date.now()}`, ...payload }) - setNewItems(payloads, () => { payloads.length = 0 }) + payloadsRef.current.push({ isError: true, time: `${Date.now()}`, ...payload }) + setNewItems(payloadsRef.current, () => { payloads.length = 0 }) dispatch(pauseMonitor()) }) // Catch disconnect - newSocket.on(SocketEvent.Disconnect, () => { + socketRef.current?.on(SocketEvent.Disconnect, () => { if (retryDelay) { - retryTimer = setTimeout(handleDisconnect, retryDelay) + retryTimerRef.current = setTimeout(handleDisconnect, retryDelay) } else { handleDisconnect() } }) // Catch connect error - newSocket.on(SocketEvent.ConnectionError, (error) => { - payloads.push({ isError: true, time: `${Date.now()}`, message: getErrorMessage(error) }) - setNewItems(payloads, () => { payloads.length = 0 }) + socketRef.current?.on(SocketEvent.ConnectionError, (error) => { + payloadsRef.current.push({ isError: true, time: `${Date.now()}`, message: getErrorMessage(error) }) + setNewItems(payloadsRef.current, () => { payloadsRef.current.length = 0 }) + }) + }, [instanceId, isRunning, isPaused]) + + useEffect(() => { + if (!isRunning) { + return + } + + socketRef.current?.removeAllListeners(SocketEvent.Connect) + socketRef.current?.on(SocketEvent.Connect, () => { + // Trigger Monitor event + clearTimeout(retryTimerRef.current!) + dispatch(setLogFileId(logFileIdRef.current)) + dispatch(setStartTimestamp(timestampRef.current)) + if (!isPaused) { + subscribeMonitorEvents() + } }) - }, [instanceId, isRunning, isSaveToFile]) + }, [isRunning, isPaused]) + + useEffect(() => { + if (!isRunning || isPaused || !socketRef.current?.connected) { + return + } + + subscribeMonitorEvents() + }, [isRunning, isPaused]) useEffect(() => { if (!isRunning) return @@ -150,6 +153,29 @@ const MonitorConfig = ({ retryDelay = 15000 } : IProps) => { } }, [socket, isRunning, isShowMonitor, isMinimizedMonitor]) + const subscribeMonitorEvents = () => { + socketRef.current?.removeAllListeners(MonitorEvent.MonitorData) + socketRef.current?.emit( + MonitorEvent.Monitor, + { logFileId: isSaveToFile ? logFileIdRef.current : null }, + handleMonitorEvents + ) + } + + const handleMonitorEvents = () => { + dispatch(setMonitorLoadingPause(false)) + socketRef.current?.on(MonitorEvent.MonitorData, (payload: IMonitorData[]) => { + payloadsRef.current = payloadsRef.current.concat(payload) + + // set batch of payloads and then clear batch + setNewItems(payloadsRef.current, () => { + payloadsRef.current.length = 0 + // reset all timings after items were changed + setNewItems.cancel() + }) + }) + } + return null } diff --git a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx index 67b385e810..1d4cf08fb6 100644 --- a/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx +++ b/redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx @@ -55,7 +55,7 @@ export const INFINITE_MESSAGES = { > Congratulations! - You can now use your Redis Stack database in Redis Enterprise Cloud + You can now use your Redis Stack database in Redis Cloud to start exploring all its developer capabilities via RedisInsight tutorials. @@ -84,7 +84,7 @@ export const INFINITE_MESSAGES = { onMouseUp={(e) => { e.preventDefault() }} data-testid="database-exists-notification" > - You already have a free Redis Enterprise Cloud subscription. + You already have a free Redis Cloud subscription. Do you want to import your existing database into RedisInsight? @@ -125,7 +125,7 @@ export const INFINITE_MESSAGES = { onMouseUp={(e) => { e.preventDefault() }} data-testid="subscription-exists-notification" > - Your subscription does not have a free Redis Enterprise Cloud database. + Your subscription does not have a free Redis Cloud database. Do you want to create a free database in your existing subscription? diff --git a/redisinsight/ui/src/components/oauth/oauth-select-account-dialog/OAuthSelectAccountDialog.tsx b/redisinsight/ui/src/components/oauth/oauth-select-account-dialog/OAuthSelectAccountDialog.tsx index af96459a90..7dcf44cb67 100644 --- a/redisinsight/ui/src/components/oauth/oauth-select-account-dialog/OAuthSelectAccountDialog.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-select-account-dialog/OAuthSelectAccountDialog.tsx @@ -125,7 +125,7 @@ const OAuthSelectAccountDialog = () => {
-

Connect to Redis Enterprise Cloud

+

Connect to Redis Cloud

Select an account to connect to: diff --git a/redisinsight/ui/src/components/oauth/oauth-sign-in-dialog/OAuthSignInDialog.tsx b/redisinsight/ui/src/components/oauth/oauth-sign-in-dialog/OAuthSignInDialog.tsx index feef06cb62..2b4eee2fa7 100644 --- a/redisinsight/ui/src/components/oauth/oauth-sign-in-dialog/OAuthSignInDialog.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-sign-in-dialog/OAuthSignInDialog.tsx @@ -44,7 +44,7 @@ const OAuthSignInDialog = () => {
-

Get started with Redis Enterprise Cloud

+

Get started with Redis Cloud

{OAuthAdvantages.map(({ icon, text, title }) => ( diff --git a/redisinsight/ui/src/components/oauth/oauth-social/OAuthSocial.tsx b/redisinsight/ui/src/components/oauth/oauth-social/OAuthSocial.tsx index ebbd8a8e63..e0a2afe6f7 100644 --- a/redisinsight/ui/src/components/oauth/oauth-social/OAuthSocial.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-social/OAuthSocial.tsx @@ -104,7 +104,7 @@ const OAuthSocial = ({ type = OAuthSocialType.Modal, hideTitle = false }: Props) Auto-discover subscriptions and add your databases.
- A new Redis Enterprise Cloud account will be created for you if you don’t have one. + A new Redis Cloud account will be created for you if you don’t have one.
{buttons} diff --git a/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx b/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx index 5dde6411fc..563eb046b5 100644 --- a/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx @@ -36,6 +36,7 @@ import { getViewTypeOptions, WBQueryType, getProfileViewTypeOptions, ProfileQuer import { IPluginVisualization } from 'uiSrc/slices/interfaces' import { RunQueryMode, ResultsMode, ResultsSummary } from 'uiSrc/slices/interfaces/workbench' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' +import { FullScreen } from 'uiSrc/components' import DefaultPluginIconDark from 'uiSrc/assets/img/workbench/default_view_dark.svg' import DefaultPluginIconLight from 'uiSrc/assets/img/workbench/default_view_light.svg' @@ -391,18 +392,7 @@ const QueryCardHeader = (props: Props) => { {(isOpen || isFullScreen) && ( - - - + )} diff --git a/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx b/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx deleted file mode 100644 index e9f2ddbab1..0000000000 --- a/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react' -import AutoSizer from 'react-virtualized-auto-sizer' -import { isArray, isEmpty } from 'lodash' -import { - TreeWalker, - TreeWalkerValue, - FixedSizeTree as Tree, -} from 'react-vtree' -import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui' -import { useDispatch } from 'react-redux' - -import { findTreeNode, getTreeLeafField, Maybe } from 'uiSrc/utils' -import { useDisposableWebworker } from 'uiSrc/services' -import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' -import { ThemeContext } from 'uiSrc/contexts/themeContext' -import { DEFAULT_DELIMITER, Theme } from 'uiSrc/constants' -import KeyLightSVG from 'uiSrc/assets/img/sidebar/browser.svg' -import KeyDarkSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' -import { resetBrowserTree } from 'uiSrc/slices/app/context' - -import { Node } from './components/Node' -import { NodeMeta, TreeData, TreeNode } from './interfaces' - -import styles from './styles.module.scss' - -export interface Props { - items: IKeyPropTypes[] - delimiter?: string - loadingIcon?: string - loading: boolean - selectDefaultLeaf?: boolean - statusSelected: { - [key: string]: { - [key: string]: IKeyPropTypes - } - }, - statusOpen: { - [key: string]: boolean - } - webworkerFn: (...args: any) => any - onSelectLeaf?: (items: any[]) => void - disableSelectDefaultLeaf?: () => void - onStatusOpen?: (name: string, value: boolean) => void - onStatusSelected?: (id: string, keys: any) => void - setConstructingTree: (status: boolean) => void -} - -export const KEYS = 'keys' - -const VirtualTree = (props: Props) => { - const { - items, - delimiter = DEFAULT_DELIMITER, - loadingIcon = 'empty', - statusOpen = {}, - statusSelected = {}, - loading, - selectDefaultLeaf, - onStatusOpen, - onStatusSelected, - onSelectLeaf, - setConstructingTree, - disableSelectDefaultLeaf, - webworkerFn = () => {} - } = props - - const { theme } = useContext(ThemeContext) - const [nodes, setNodes] = useState([]) - const { result, run: runWebworker } = useDisposableWebworker(webworkerFn) - - const dispatch = useDispatch() - - useEffect(() => - () => setNodes([]), - []) - - // receive result from the "runWebworker" - useEffect(() => { - if (!result) { - return - } - - setNodes(result) - setConstructingTree?.(false) - }, [result]) - - // select "root" Keys after render a new tree (construct a tree) - useEffect(() => { - if (nodes.length === 0 || !selectDefaultLeaf || loading) { - return - } - - if (isArray(nodes) && isEmpty(statusSelected)) { - let selectedLeaf: Maybe = nodes?.find(({ children = [] }) => children.length === 0) - - // if Keys folder not exists - first folder should be opened - if (!selectedLeaf && nodes.length) { - selectedLeaf = nodes?.[0] - - onStatusOpen?.(selectedLeaf?.fullName ?? '', true) - onStatusSelected?.( - `${selectedLeaf?.fullName + KEYS + delimiter + KEYS + delimiter}` ?? '', - selectedLeaf?.keys ?? selectedLeaf?.children?.[0]?.keys - ) - } else { - // if Keys folder exist - open it - onStatusSelected?.(selectedLeaf?.fullName ?? '', selectedLeaf?.keys) - } - - disableSelectDefaultLeaf?.() - onSelectLeaf?.(selectedLeaf?.keys ?? selectedLeaf?.children?.[0]?.keys ?? []) - } - }, [nodes, loading, selectDefaultLeaf]) - - useEffect(() => { - if (isEmpty(statusSelected) || !nodes.length) { - return - } - - // if selected Keys folder is not exists (after a new search) needs reset Browser state - const selectedLeafExists = !!findTreeNode(nodes, Object.keys(statusSelected)?.[0], 'fullName') - - if (!selectedLeafExists) { - dispatch(resetBrowserTree()) - } - }, [nodes]) - - useEffect(() => { - if (!items?.length) { - setNodes([]) - runWebworker?.({ items: [], delimiter }) - return - } - - setConstructingTree(true) - runWebworker?.({ items, delimiter }) - }, [items, delimiter]) - - const handleSelectLeaf = useCallback((keys: any[]) => { - onSelectLeaf?.(keys) - }, [onSelectLeaf]) - - const handleUpdateSelected = useCallback((fullName: string, keys: any) => { - onStatusSelected?.(fullName, keys) - }, [onStatusSelected]) - - const handleUpdateOpen = useCallback((name: string, value: boolean) => { - onStatusOpen?.(name, value) - }, [onStatusOpen]) - - // This helper function constructs the object that will be sent back at the step - // [2] during the treeWalker function work. Except for the mandatory `data` - // field you can put any additional data here. - const getNodeData = ( - node: TreeNode, - nestingLevel: number, - ): TreeWalkerValue => ({ - data: { - id: node.id.toString(), - isLeaf: node.children?.length === 0, - keyCount: node.keyCount, - name: node.name, - fullName: node.fullName, - nestingLevel, - setItems: handleSelectLeaf, - updateStatusSelected: handleUpdateSelected, - updateStatusOpen: handleUpdateOpen, - leafIcon: theme === Theme.Dark ? KeyDarkSVG : KeyLightSVG, - keyApproximate: node.keyApproximate, - keys: node.keys || node?.[getTreeLeafField(delimiter)], - isSelected: Object.keys(statusSelected)[0] === node.fullName, - isOpenByDefault: statusOpen[node.fullName], - }, - nestingLevel, - node, - }) - - // The `treeWalker` function runs only on tree re-build which is performed - // whenever the `treeWalker` prop is changed. - const treeWalker = useCallback( - function* treeWalker(): ReturnType> { - // Step [1]: Define the root multiple nodes of our tree - for (let i = 0; i < nodes.length; i++) { - yield getNodeData(nodes[i], 0) - } - - // Step [2]: Get the parent component back. It will be the object - // the `getNodeData` function constructed, so you can read any data from it. - while (true) { - const parentMeta = yield - - for (let i = 0; i < parentMeta.node.children?.length; i++) { - // Step [3]: Yielding all the children of the provided component. Then we - // will return for the step [2] with the first children. - yield getNodeData( - parentMeta.node.children[i], - parentMeta.nestingLevel + 1, - ) - } - } - }, - [nodes, statusSelected], - ) - - return ( - - {({ height, width }) => ( -
- { nodes.length > 0 && ( - - {Node} - - )} - { nodes.length === 0 && loading && ( -
-
- - -
-
- )} -
- )} -
- ) -} - -export default VirtualTree diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx b/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx deleted file mode 100644 index 11850e3968..0000000000 --- a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useEffect } from 'react' -import { NodePublicState } from 'react-vtree/dist/es/Tree' -import cx from 'classnames' -import { EuiIcon, EuiToolTip, keys as ElasticKeys } from '@elastic/eui' - -import { TreeData } from '../../interfaces' -import styles from './styles.module.scss' - -// Node component receives all the data we created in the `treeWalker` + -// internal openness state (`isOpen`), function to change internal openness -// `style` parameter that should be added to the root div. -const Node = ({ - data, - isOpen, - style, - setOpen -}: NodePublicState) => { - const { - isLeaf, - leafIcon, - keys, - name, - keyCount, - nestingLevel, - fullName, - keyApproximate, - isSelected, - setItems, - updateStatusOpen, - updateStatusSelected, - } = data - - useEffect(() => { - if (isSelected && keys) { - updateStatusSelected?.(fullName, keys) - } - }, [keys, isSelected]) - - const handleClick = () => { - if (isLeaf && keys && !isSelected) { - setItems?.(keys) - updateStatusSelected?.(fullName, keys) - } - - updateStatusOpen?.(fullName, !isOpen) - - !isLeaf && setOpen(!isOpen) - } - - const handleKeyDown = ({ key }: React.KeyboardEvent) => { - if (key === ElasticKeys.SPACE) { - handleClick() - } - } - - const Node = ( -
{}} - data-testid={`node-item_${fullName}`} - > -
- {!isLeaf && ( - <> - - - {name} - - )} - - {isLeaf && ( - <> - - Keys - - )} -
-
- {keyCount ?? ''} - - {keyApproximate ? `${keyApproximate < 1 ? '<1' : Math.round(keyApproximate)}%` : '' } - -
-
- ) - - const content = ( - <> - {`${fullName}*`} -
- {`${keyCount} key(s) (${Math.round(keyApproximate * 100) / 100}%)`} - - ) - - return ( -
- {isLeaf && Node} - {!isLeaf && ( - - {Node} - - )} -
- ) -} - -export default Node diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/styles.module.scss b/redisinsight/ui/src/components/virtual-tree/components/Node/styles.module.scss deleted file mode 100644 index 38e9aabe51..0000000000 --- a/redisinsight/ui/src/components/virtual-tree/components/Node/styles.module.scss +++ /dev/null @@ -1,73 +0,0 @@ -.anchorTooltipNode { - width: 100%; - display: inline-block; - position: relative; -} - -.nodeContainer { - border-left: 3px solid transparent; - - &:hover { - background-color: var(--browserComponentActive); - } -} - -.nodeContent { - display: flex; - justify-content: space-between; - cursor: pointer; - padding-right: 8px; - color: var(--euiTextSubduedColor) !important; - font: normal normal normal 13px/28px Graphik, sans-serif !important; - letter-spacing: -0.13px; - white-space: nowrap; - - &Open { - color: var(--euiColorFullShade) !important; - } -} - -.nodeSelected { - border-left-color: var(--euiColorPrimary) !important; - background-color: var(--browserComponentActive); - - .nodeContent { - color: var(--euiColorFullShade) !important; - } -} - -.nodeName { - position: relative; - overflow: hidden; - text-overflow: ellipsis; -} - -.nodeIcon { - margin-right: 8px; - - &Arrow { - margin-left: 8px; - margin-right: 6px; - width: 10px !important; - height: 10px !important; - } - - &Leaf { - margin-left: 25px; - margin-bottom: 2px; - width: 14px; - height: 14px; - } -} - -.approximate { - display: inline-block; - width: 36px; - text-align: end; -} - -.options { - padding-left: 12px; - font-size: 12px; - font-weight: 300; -} diff --git a/redisinsight/ui/src/constants/browser.ts b/redisinsight/ui/src/constants/browser.ts index d4e7bc57b5..555ea94332 100644 --- a/redisinsight/ui/src/constants/browser.ts +++ b/redisinsight/ui/src/constants/browser.ts @@ -1,6 +1,7 @@ -import { KeyValueFormat } from './keys' +import { KeyValueFormat, SortOrder } from './keys' export const DEFAULT_DELIMITER = ':' +export const DEFAULT_TREE_SORTING = SortOrder.ASC export const DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS = false export const TEXT_UNPRINTABLE_CHARACTERS = { diff --git a/redisinsight/ui/src/constants/browser/keyDetailsHeader.ts b/redisinsight/ui/src/constants/browser/keyDetailsHeader.ts new file mode 100644 index 0000000000..5949905914 --- /dev/null +++ b/redisinsight/ui/src/constants/browser/keyDetailsHeader.ts @@ -0,0 +1,3 @@ +const PADDING_WRAPPER_SIZE = 36 +export const HIDE_LAST_REFRESH = 850 - PADDING_WRAPPER_SIZE +export const MIDDLE_SCREEN_RESOLUTION = 740 - PADDING_WRAPPER_SIZE diff --git a/redisinsight/ui/src/constants/bulkActions.ts b/redisinsight/ui/src/constants/bulkActions.ts index 5a2d6b3945..7a8cea7403 100644 --- a/redisinsight/ui/src/constants/bulkActions.ts +++ b/redisinsight/ui/src/constants/bulkActions.ts @@ -20,6 +20,7 @@ export enum BulkActionsStatus { Completed = 'completed', Failed = 'failed', Aborted = 'aborted', + Disconnected = 'disconnected' } export const MAX_BULK_ACTION_ERRORS_LENGTH = 500 diff --git a/redisinsight/ui/src/constants/index.ts b/redisinsight/ui/src/constants/index.ts index caac0c1151..6ae5c0a6c8 100644 --- a/redisinsight/ui/src/constants/index.ts +++ b/redisinsight/ui/src/constants/index.ts @@ -31,4 +31,5 @@ export * from './serverVersions' export * from './customErrorCodes' export * from './securityField' export * from './redisearch' +export * from './browser/keyDetailsHeader' export { ApiEndpoints, BrowserStorageItem, ApiStatusCode, apiErrors } diff --git a/redisinsight/ui/src/constants/keys.ts b/redisinsight/ui/src/constants/keys.ts index 94310dd617..1ae5ccaafe 100644 --- a/redisinsight/ui/src/constants/keys.ts +++ b/redisinsight/ui/src/constants/keys.ts @@ -90,38 +90,6 @@ export type KeyTypesActions = { } } -export const KEY_TYPES_ACTIONS: KeyTypesActions = Object.freeze({ - [KeyTypes.Hash]: { - addItems: { - name: 'Add Fields', - }, - }, - [KeyTypes.List]: { - addItems: { - name: 'Add Element', - }, - removeItems: { - name: 'Remove Elements', - }, - }, - [KeyTypes.Set]: { - addItems: { - name: 'Add Members', - }, - }, - [KeyTypes.ZSet]: { - addItems: { - name: 'Add Members', - }, - }, - [KeyTypes.String]: { - editItem: { - name: 'Edit Value', - }, - }, - [KeyTypes.ReJSON]: {} -}) - export const STREAM_ADD_GROUP_VIEW_TYPES = [ StreamViewType.Groups, StreamViewType.Consumers, diff --git a/redisinsight/ui/src/constants/mocks/mock-recommendations.ts b/redisinsight/ui/src/constants/mocks/mock-recommendations.ts index 08ed30a555..3ab6677f76 100644 --- a/redisinsight/ui/src/constants/mocks/mock-recommendations.ts +++ b/redisinsight/ui/src/constants/mocks/mock-recommendations.ts @@ -414,7 +414,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.com/redis-enterprise-cloud/overview/', - name: 'Redis Enterprise Cloud' + name: 'Redis Cloud' } }, { @@ -558,7 +558,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.com/redis-enterprise-cloud/overview/', - name: 'Redis Enterprise Cloud' + name: 'Redis Cloud' } }, { @@ -719,7 +719,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.com/redis-enterprise-cloud/overview/', - name: 'Redis Enterprise Cloud' + name: 'Redis Cloud' } }, { @@ -997,7 +997,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.com/redis-enterprise-cloud/overview/', - name: 'Redis Enterprise Cloud' + name: 'Redis Cloud' } }, { @@ -1089,7 +1089,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.com/redis-enterprise-cloud/overview/', - name: 'Redis Enterprise Cloud' + name: 'Redis Cloud' } }, { @@ -1218,7 +1218,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.com/redis-enterprise-cloud/overview/', - name: 'Redis Enterprise Cloud' + name: 'Redis Cloud' } }, { @@ -1399,7 +1399,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.com/redis-enterprise-cloud/overview/', - name: 'Redis Enterprise Cloud' + name: 'Redis Cloud' } }, { @@ -1479,7 +1479,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.com/redis-enterprise-cloud/overview/', - name: 'Redis Enterprise Cloud' + name: 'Redis Cloud' } }, { @@ -1548,7 +1548,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.com/redis-enterprise-cloud/overview/', - name: 'Redis Enterprise Cloud' + name: 'Redis Cloud' } }, { @@ -1625,7 +1625,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.com/redis-enterprise-cloud/overview/', - name: 'Redis Enterprise Cloud' + name: 'Redis Cloud' } }, { diff --git a/redisinsight/ui/src/constants/storage.ts b/redisinsight/ui/src/constants/storage.ts index be27828c17..4627732ac4 100644 --- a/redisinsight/ui/src/constants/storage.ts +++ b/redisinsight/ui/src/constants/storage.ts @@ -13,6 +13,7 @@ enum BrowserStorageItem { wbInputHistory = 'wbInputHistory', isEnablementAreaMinimized = 'isEnablementAreaMinimized', treeViewDelimiter = 'treeViewDelimiter', + treeViewSort = 'treeViewSort', autoRefreshRate = 'autoRefreshRate', bulkActionDeleteId = 'bulkActionDeleteId', dbConfig = 'dbConfig_', diff --git a/redisinsight/ui/src/constants/texts.tsx b/redisinsight/ui/src/constants/texts.tsx index 1ce0beeca2..f8881ed31a 100644 --- a/redisinsight/ui/src/constants/texts.tsx +++ b/redisinsight/ui/src/constants/texts.tsx @@ -1,8 +1,19 @@ import React from 'react' import { EuiText, EuiSpacer } from '@elastic/eui' -export const NoResultsFoundText = (No results found.) -export const NoSelectedIndexText = (Select an index and enter a query to search per values of keys.) +export const NoResultsFoundText = ( + + No results found. + +) +export const NoSelectedIndexText = ( + + Select an index and enter a query to search per values of keys. + +) export const FullScanNoResultsFoundText = ( <> @@ -19,7 +30,7 @@ export const FullScanNoResultsFoundText = ( ) export const ScanNoResultsFoundText = ( <> - No results found. + No results found.
Use "Scan more" button to proceed or filter per exact Key Name to scan more efficiently. diff --git a/redisinsight/ui/src/helpers/constructKeysToTree.ts b/redisinsight/ui/src/helpers/constructKeysToTree.ts index 70b23501ee..d75559b94b 100644 --- a/redisinsight/ui/src/helpers/constructKeysToTree.ts +++ b/redisinsight/ui/src/helpers/constructKeysToTree.ts @@ -1,12 +1,14 @@ +import { SortOrder } from 'uiSrc/constants' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' interface Props { items: IKeyPropTypes[] delimiter?: string + sorting?: SortOrder } export const constructKeysToTree = (props: Props): any[] => { - const { items: keys, delimiter = ':' } = props + const { items: keys, delimiter = ':', sorting = 'ASC' } = props const keysSymbol = `keys${delimiter}keys` const tree: any = {} @@ -20,11 +22,8 @@ export const constructKeysToTree = (props: Props): any[] => { nameSplitted.forEach((value:any, index: number) => { // create a key leaf if (index === lastIndex) { - if (currentNode[keysSymbol] === undefined) { - currentNode[keysSymbol] = {} - } - - currentNode[keysSymbol][name] = key + // eslint-disable-next-line prefer-object-spread + currentNode[name + keysSymbol] = Object.assign({}, key, { isLeaf: true }) } else if (currentNode[value] === undefined) { currentNode[value] = {} } @@ -47,39 +46,61 @@ export const constructKeysToTree = (props: Props): any[] => { return candidateId } - // FormatTreeData - const formatTreeData = (tree: any, previousKey = '', delimiter = ':') => { - const treeNodes = Reflect.ownKeys(tree) - - // sort Ungrouped Keys group to top - treeNodes.some((key, index) => { - if (key === keysSymbol) { - const temp = treeNodes[0] - treeNodes[0] = key - treeNodes[index] = temp - return true + // Folders should be always before leaves + const sortKeysAndFolder = (nodes: string[]) => { + nodes.sort((a, b) => { + // Custom sorting for items ending with "keys:keys" + if (a.endsWith(keysSymbol) && !b.endsWith(keysSymbol)) { + return 1 + } + if (!a.endsWith(keysSymbol) && b.endsWith(keysSymbol)) { + return -1 + } + + // Regular sorting + if (sorting === 'ASC') { + return a.localeCompare(b, 'en', { numeric: true }) + } + if (sorting === 'DESC') { + return b.localeCompare(a, 'en', { numeric: true }) } - return false + + return 0 }) + } + + // FormatTreeData + const formatTreeData = (tree: any, previousKey = '', delimiter = ':', prevIndex = '') => { + const treeNodes: string[] = Object.keys(tree) + + sortKeysAndFolder(treeNodes) - return treeNodes.map((key) => { + return treeNodes.map((key, index) => { const name = key?.toString() - const node: any = { name } + const node: any = { nameString: name } const tillNowKeyName = previousKey + name + delimiter + const path = prevIndex ? `${prevIndex}.${index}` : `${index}` // populate node with children nodes - if (key !== keysSymbol && Reflect.ownKeys(tree[key]).length > 0) { - node.children = formatTreeData(tree[key], tillNowKeyName, delimiter) - node.keyCount = node.children.reduce((a: any, b:any) => a + (b.keyCount || 0), 0) + if (!tree[key].isLeaf && Object.keys(tree[key]).length > 0) { + node.children = formatTreeData( + tree[key], + tillNowKeyName, + delimiter, + path, + ) + node.keyCount = node.children.reduce((a: any, b:any) => a + (b.keyCount || 1), 0) + node.keyApproximate = (node.keyCount / keys.length) * 100 } else { - // populate leaf with keys + // populate leaf + node.isLeaf = true node.children = [] - node.keys = tree[keysSymbol] ?? [] - node.keyCount = Object.keys(node.keys ?? [])?.length ?? 1 + node.nameString = name.slice(0, -keysSymbol.length) + node.nameBuffer = tree[key]?.name } + node.path = path node.fullName = tillNowKeyName - node.keyApproximate = (node.keyCount / keys.length) * 100 node.id = getUniqueId() return node }) diff --git a/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts b/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts index 0ced2705c5..0bce8a266c 100644 --- a/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts +++ b/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts @@ -1,133 +1,108 @@ -import { DEFAULT_DELIMITER } from 'uiSrc/constants' -import { getTreeLeafField } from 'uiSrc/utils' - export const constructKeysToTreeMockResult = [ { - name: getTreeLeafField(DEFAULT_DELIMITER), - children: [], - keys: { - keys2: { - nameString: 'keys2', - type: 'hash', - ttl: -1, - size: 71 - }, - test1: { - nameString: 'test1', - type: 'hash', - ttl: -1, - size: 71 - }, - test2: { - nameString: 'test2', - type: 'hash', - ttl: -1, - size: 71 - }, - keys1: { - nameString: 'keys1', - type: 'hash', - ttl: -1, - size: 71 - } - }, - keyCount: 4, - fullName: `${getTreeLeafField(DEFAULT_DELIMITER)}:`, - keyApproximate: 40, - }, - { - name: 'keys', + nameString: 'empty', children: [ { - name: getTreeLeafField(DEFAULT_DELIMITER), - children: [], - keys: { - 'keys:1': { - nameString: 'keys:1', - type: 'hash', - ttl: -1, - size: 71 - }, - 'keys:3': { - nameString: 'keys:3', - type: 'hash', - ttl: -1, - size: 71 - }, - 'keys:2': { - nameString: 'keys:2', - type: 'hash', - ttl: -1, - size: 71 - } - }, - keyCount: 3, - fullName: `keys:${getTreeLeafField(DEFAULT_DELIMITER)}:`, - keyApproximate: 30, - }, - { - name: '1', + nameString: '', children: [ { - name: getTreeLeafField(DEFAULT_DELIMITER), + nameString: 'empty::test', + isLeaf: true, children: [], - keys: { - 'keys:1:2': { - nameString: 'keys:1:2', - type: 'hash', - ttl: -1, - size: 71 - }, - 'keys:1:1': { - nameString: 'keys:1:1', - type: 'hash', - ttl: -1, - size: 71 - } - }, - keyCount: 2, - fullName: `keys:1:${getTreeLeafField(DEFAULT_DELIMITER)}:`, - keyApproximate: 20, + path: '0.0.0', + fullName: 'empty::empty::testkeys:keys:', } ], - keyCount: 2, - fullName: 'keys:1:', - keyApproximate: 20, + keyCount: 1, + keyApproximate: 10, + path: '0.0', + fullName: 'empty::', } ], - keyCount: 5, - fullName: 'keys:', - keyApproximate: 50, + keyCount: 1, + keyApproximate: 10, + path: '0', + fullName: 'empty:', }, { - name: 'empty', + nameString: 'keys', children: [ { - name: '', + nameString: '1', children: [ { - name: getTreeLeafField(DEFAULT_DELIMITER), + nameString: 'keys:1:1', + isLeaf: true, + children: [], + path: '1.0.0', + fullName: 'keys:1:keys:1:1keys:keys:', + }, + { + nameString: 'keys:1:2', + isLeaf: true, children: [], - keys: { - 'empty::test': { - nameString: 'empty::test', - type: 'hash', - ttl: -1, - size: 71 - } - }, - keyCount: 1, - fullName: `empty::${getTreeLeafField(DEFAULT_DELIMITER)}:`, - keyApproximate: 10, + path: '1.0.1', + fullName: 'keys:1:keys:1:2keys:keys:', } ], - keyCount: 1, - fullName: 'empty::', - keyApproximate: 10, + keyCount: 2, + keyApproximate: 20, + path: '1.0', + fullName: 'keys:1:', + }, + { + nameString: 'keys:1', + isLeaf: true, + children: [], + path: '1.1', + fullName: 'keys:keys:1keys:keys:', + }, + { + nameString: 'keys:2', + isLeaf: true, + children: [], + path: '1.2', + fullName: 'keys:keys:2keys:keys:', + }, + { + nameString: 'keys:3', + isLeaf: true, + children: [], + path: '1.3', + fullName: 'keys:keys:3keys:keys:', } ], - keyCount: 1, - fullName: 'empty:', - keyApproximate: 10, + keyCount: 5, + keyApproximate: 50, + path: '1', + fullName: 'keys:', + }, + { + nameString: 'keys1', + isLeaf: true, + children: [], + path: '2', + fullName: 'keys1keys:keys:', + }, + { + nameString: 'keys2', + isLeaf: true, + children: [], + path: '3', + fullName: 'keys2keys:keys:', + }, + { + nameString: 'test1', + isLeaf: true, + children: [], + path: '4', + fullName: 'test1keys:keys:', + }, + { + nameString: 'test2', + isLeaf: true, + children: [], + path: '5', + fullName: 'test2keys:keys:', } ] diff --git a/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx b/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx index 33f914415d..9279901966 100644 --- a/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx +++ b/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx @@ -13,9 +13,10 @@ import { } from 'uiSrc/slices/app/context' import BrowserPage from './BrowserPage' import KeyList, { Props as KeyListProps } from './components/key-list/KeyList' -import KeyDetailsWrapper, { +import { Props as KeyDetailsWrapperProps -} from './components/key-details/KeyDetailsWrapper' +} from './modules/key-details/KeyDetails' +import { KeyDetails } from './modules' import AddKey, { Props as AddKeyProps } from './components/add-key/AddKey' import BrowserSearchPanel from './components/browser-search-panel' import { Props as KeysHeaderProps } from './components/keys-header/KeysHeader' @@ -32,10 +33,9 @@ jest.mock('./components/add-key/AddKey', () => ({ default: jest.fn(), })) -jest.mock('./components/key-details/KeyDetailsWrapper', () => ({ +jest.mock('uiSrc/pages/browser/modules', () => ({ __esModule: true, - namedExport: jest.fn(), - default: jest.fn(), + KeyDetails: jest.fn(), })) jest.mock('./components/browser-search-panel', () => ({ @@ -103,7 +103,7 @@ describe('BrowserPage', () => { beforeAll(() => { KeyList.mockImplementation(mockKeyList) BrowserSearchPanel.mockImplementation(mockBrowserSearchPanel) - KeyDetailsWrapper.mockImplementation(mockKeyDetailsWrapper) + KeyDetails.mockImplementation(mockKeyDetailsWrapper) AddKey.mockImplementation(mockAddKey) }) diff --git a/redisinsight/ui/src/pages/browser/BrowserPage.tsx b/redisinsight/ui/src/pages/browser/BrowserPage.tsx index a05f8097f5..f87e868291 100644 --- a/redisinsight/ui/src/pages/browser/BrowserPage.tsx +++ b/redisinsight/ui/src/pages/browser/BrowserPage.tsx @@ -190,7 +190,7 @@ const BrowserPage = () => { } const selectKey = ({ rowData }: { rowData: any }) => { - if (!isEqualBuffers(rowData.name, selectedKey)) { + if (!isEqualBuffers(rowData.name, selectedKeyRef.current)) { dispatch(toggleBrowserFullScreen(false)) dispatch(setInitialStateByType(prevSelectedType.current)) diff --git a/redisinsight/ui/src/pages/browser/components/add-items-actions/AddItemsActions.spec.tsx b/redisinsight/ui/src/pages/browser/components/add-items-actions/AddItemsActions.spec.tsx index 074d498412..e9ec3607b2 100644 --- a/redisinsight/ui/src/pages/browser/components/add-items-actions/AddItemsActions.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-items-actions/AddItemsActions.spec.tsx @@ -1,8 +1,7 @@ import React from 'react' -import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' import { instance, mock } from 'ts-mockito' -import AddItemsActions from './AddItemsActions' -import { Props } from '../key-details-add-items/add-zset-members/AddZsetMembers' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' +import AddItemsActions, { Props } from './AddItemsActions' const mockedProps = mock() 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 48c5955d42..7116935e11 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 @@ -1,14 +1,14 @@ import React from 'react' import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui' -interface Props { +export interface Props { id: number length: number index: number loading: boolean removeItem: (id: number) => void addItem: () => void - anchorClassName: string + anchorClassName?: string clearItemValues?: (id: number) => void clearIsDisabled?: boolean addItemIsDisabled?: boolean diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/AddKeyHash.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/AddKeyHash.tsx index abf1e3ec46..0f643813e1 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/AddKeyHash.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/AddKeyHash.tsx @@ -14,14 +14,11 @@ import { import { addHashKey, addKeyStateSelector, } from 'uiSrc/slices/browser/keys' -import { - IHashFieldState, - INITIAL_HASH_FIELD_STATE -} from 'uiSrc/pages/browser/components/key-details-add-items/add-hash-fields/AddHashFields' + import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' -import styles from 'uiSrc/pages/browser/components/key-details-add-items/styles.module.scss' import { Maybe, stringToBuffer } from 'uiSrc/utils' +import { IHashFieldState, INITIAL_HASH_FIELD_STATE } from 'uiSrc/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields' import { CreateHashWithExpireDto } from 'apiSrc/modules/browser/dto/hash.dto' import { AddHashFormConfig as config @@ -186,7 +183,6 @@ const AddKeyHash = (props: Props) => { clearItemValues={clearFieldsValues} clearIsDisabled={isClearDisabled(item)} loading={loading} - anchorClassName={styles.refreshKeyTooltip} />
diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.tsx index 3a41e2e04a..465603ab8f 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.tsx @@ -16,6 +16,8 @@ import { addSetKey, addKeyStateSelector, } from 'uiSrc/slices/browser/keys' import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' + +import { INITIAL_SET_MEMBER_STATE, ISetMemberState } from 'uiSrc/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers' import { CreateSetWithExpireDto } from 'apiSrc/modules/browser/dto/set.dto' import { @@ -23,13 +25,6 @@ import { } from '../constants/fields-config' import AddKeyFooter from '../AddKeyFooter/AddKeyFooter' -import { - INITIAL_SET_MEMBER_STATE, - ISetMemberState -} from '../../key-details-add-items/add-set-members/AddSetMembers' - -import styles from '../../key-details-add-items/styles.module.scss' - export interface Props { keyName: string keyTTL: Maybe @@ -166,7 +161,6 @@ const AddKeySet = (props: Props) => { clearIsDisabled={isClearDisabled(item)} clearItemValues={clearMemberValues} loading={loading} - anchorClassName={styles.refreshKeyTooltip} /> diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.tsx index c538289436..e627d9afce 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.tsx @@ -10,8 +10,8 @@ import { } from '@elastic/eui' import { addStreamKey } from 'uiSrc/slices/browser/keys' import { entryIdRegex, isRequiredStringsValid, Maybe, stringToBuffer } from 'uiSrc/utils' -import { StreamEntryFields } from 'uiSrc/pages/browser/components/key-details-add-items' import { AddStreamFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' +import { StreamEntryFields } from 'uiSrc/pages/browser/modules/key-details/components/stream-details/add-stream-entity' import { CreateStreamDto } from 'apiSrc/modules/browser/dto/stream.dto' import AddKeyFooter from '../AddKeyFooter/AddKeyFooter' diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.tsx index 9156369ea5..4cc7ec7e73 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.tsx @@ -17,16 +17,12 @@ import { isNaNConvertedString } from 'uiSrc/utils/numbers' import { addZsetKey, addKeyStateSelector } from 'uiSrc/slices/browser/keys' import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' -import styles from 'uiSrc/pages/browser/components/key-details-add-items/styles.module.scss' + +import { INITIAL_ZSET_MEMBER_STATE, IZsetMemberState } from 'uiSrc/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers' import { CreateZSetWithExpireDto } from 'apiSrc/modules/browser/dto/z-set.dto' import AddKeyFooter from '../AddKeyFooter/AddKeyFooter' import { AddZsetFormConfig as config } from '../constants/fields-config' -import { - INITIAL_ZSET_MEMBER_STATE, - IZsetMemberState -} from '../../key-details-add-items/add-zset-members/AddZsetMembers' - export interface Props { keyName: string keyTTL: Maybe @@ -233,7 +229,6 @@ const AddKeyZset = (props: Props) => { addItemIsDisabled={(members.some((item) => !item.score.length))} clearItemValues={clearMemberValues} loading={loading} - anchorClassName={styles.refreshKeyTooltip} /> diff --git a/redisinsight/ui/src/pages/browser/components/browser-left-panel/BrowserLeftPanel.tsx b/redisinsight/ui/src/pages/browser/components/browser-left-panel/BrowserLeftPanel.tsx index df1ad59ddc..17b02065e1 100644 --- a/redisinsight/ui/src/pages/browser/components/browser-left-panel/BrowserLeftPanel.tsx +++ b/redisinsight/ui/src/pages/browser/components/browser-left-panel/BrowserLeftPanel.tsx @@ -56,6 +56,7 @@ const BrowserLeftPanel = (props: Props) => { searchMode, isSearched: patternIsSearched, filter, + deleting, } = useSelector(keysSelector) const { contextInstanceId } = useSelector(appContextSelector) const { @@ -154,6 +155,7 @@ const BrowserLeftPanel = (props: Props) => { selectKey={selectKey} loadMoreItems={loadMoreItems} onDelete={onDeleteKey} + deleting={deleting} onAddKeyPanel={handleAddKeyPanel} onBulkActionsPanel={handleBulkActionsPanel} /> diff --git a/redisinsight/ui/src/pages/browser/components/browser-right-panel/BrowserRightPanel.tsx b/redisinsight/ui/src/pages/browser/components/browser-right-panel/BrowserRightPanel.tsx index 50fb490b05..ec91873c5b 100644 --- a/redisinsight/ui/src/pages/browser/components/browser-right-panel/BrowserRightPanel.tsx +++ b/redisinsight/ui/src/pages/browser/components/browser-right-panel/BrowserRightPanel.tsx @@ -6,9 +6,8 @@ import { useParams } from 'react-router-dom' import AddKey from 'uiSrc/pages/browser/components/add-key/AddKey' import BulkActions from 'uiSrc/pages/browser/components/bulk-actions' import CreateRedisearchIndex from 'uiSrc/pages/browser/components/create-redisearch-index/' -import KeyDetailsWrapper from 'uiSrc/pages/browser/components/key-details/KeyDetailsWrapper' +import { KeyDetails } from 'uiSrc/pages/browser/modules' -import { updateBrowserTreeSelectedLeaf } from 'uiSrc/slices/app/context' import { keysDataSelector, keysSelector, @@ -16,9 +15,8 @@ import { toggleBrowserFullScreen } from 'uiSrc/slices/browser/keys' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { KeyViewType } from 'uiSrc/slices/interfaces/keys' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { bufferToString, Nullable } from 'uiSrc/utils' +import { Nullable } from 'uiSrc/utils' export interface Props { selectedKey: Nullable @@ -91,10 +89,6 @@ const BrowserRightPanel = (props: Props) => { const handleEditKey = (key: RedisResponseBuffer, newKey: RedisResponseBuffer) => { setSelectedKey(newKey) - - if (viewType === KeyViewType.Tree) { - dispatch(updateBrowserTreeSelectedLeaf({ key: bufferToString(key), newKey: bufferToString(newKey) })) - } } const onEditKey = useCallback( @@ -110,7 +104,7 @@ const BrowserRightPanel = (props: Props) => { return ( <> {every([!isAddKeyPanelOpen, !isBulkActionsPanelOpen, !isCreateIndexPanelOpen], Boolean) && ( - {

Bulk Actions

{!arePanelsCollapsed && ( - - - + /> )} {(!arePanelsCollapsed || isFullScreen) && ( { expect(screen.queryByTestId('bulk-actions-info-filter')).not.toBeInTheDocument() }) + + it('should show connection lost when status is disconnect', () => { + render() + + expect(screen.getByTestId('bulk-status-disconnected')).toHaveTextContent('Connection Lost') + }) }) diff --git a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsInfo/BulkActionsInfo.tsx b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsInfo/BulkActionsInfo.tsx index c46b95c658..141ca2e9e8 100644 --- a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsInfo/BulkActionsInfo.tsx +++ b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsInfo/BulkActionsInfo.tsx @@ -7,6 +7,7 @@ import { getApproximatePercentage, Maybe, Nullable } from 'uiSrc/utils' import Divider from 'uiSrc/components/divider/Divider' import { BulkActionsStatus, KeyTypes } from 'uiSrc/constants' import GroupBadge from 'uiSrc/components/group-badge/GroupBadge' +import { isProcessedBulkAction } from 'uiSrc/pages/browser/components/bulk-actions/utils' import styles from './styles.module.scss' export interface Props { @@ -46,7 +47,7 @@ const BulkActionsInfo = (props: Props) => {
)}
- {!isUndefined(status) && status !== BulkActionsStatus.Completed && status !== BulkActionsStatus.Aborted && ( + {!isUndefined(status) && !isProcessedBulkAction(status) && ( In progress: {` ${getApproximatePercentage(total, scanned)}`} @@ -62,6 +63,11 @@ const BulkActionsInfo = (props: Props) => { Action completed )} + {status === BulkActionsStatus.Disconnected && ( + + Connection Lost: {getApproximatePercentage(total, scanned)} + + )}
{loading && ( diff --git a/redisinsight/ui/src/pages/browser/components/bulk-actions/utils.ts b/redisinsight/ui/src/pages/browser/components/bulk-actions/utils.ts index 04ee7430d0..74bca9c106 100644 --- a/redisinsight/ui/src/pages/browser/components/bulk-actions/utils.ts +++ b/redisinsight/ui/src/pages/browser/components/bulk-actions/utils.ts @@ -9,3 +9,4 @@ export const isProcessedBulkAction = (status?: BulkActionsStatus) => status === BulkActionsStatus.Completed || status === BulkActionsStatus.Aborted || status === BulkActionsStatus.Failed + || status === BulkActionsStatus.Disconnected diff --git a/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx b/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx index 2f131f4361..43585a4b64 100644 --- a/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx +++ b/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx @@ -18,6 +18,7 @@ import { isVersionHigherOrEquals } from 'uiSrc/utils' import { KeyViewType } from 'uiSrc/slices/interfaces/keys' import { FilterNotAvailable } from 'uiSrc/components' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { resetBrowserTree } from 'uiSrc/slices/app/context' import { FILTER_KEY_TYPE_OPTIONS } from './constants' import styles from './styles.module.scss' @@ -74,6 +75,9 @@ const FilterKeyType = () => { setTypeSelected(value) setIsSelectOpen(false) dispatch(setFilter(value === ALL_KEY_TYPES_VALUE ? null : value)) + if (viewType === KeyViewType.Tree) { + dispatch(resetBrowserTree()) + } dispatch( fetchKeys( { diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/index.ts b/redisinsight/ui/src/pages/browser/components/key-details-add-items/index.ts deleted file mode 100644 index c389c49cbe..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import AddHashFields from './add-hash-fields/AddHashFields' -import AddListElements from './add-list-elements/AddListElements' -import AddSetMembers from './add-set-members/AddSetMembers' -import AddStreamEntries, { StreamEntryFields } from './add-stream-entity' -import AddStreamGroup from './add-stream-group' -import AddZsetMembers from './add-zset-members/AddZsetMembers' - -export { - AddHashFields, - AddListElements, - AddSetMembers, - AddStreamEntries, - StreamEntryFields, - AddZsetMembers, - AddStreamGroup -} diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-details-add-items/styles.module.scss deleted file mode 100644 index aa5b89a30c..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/styles.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.content { - display: flex; - flex-direction: column; - max-height: 184px; - width: 100%; - border: none !important; - border-top: 1px solid var(--euiColorPrimary); - padding: 12px 20px; - scroll-padding-bottom: 72px; -} diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx b/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx deleted file mode 100644 index bc1d08ff9f..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.tsx +++ /dev/null @@ -1,724 +0,0 @@ -import { - EuiButton, - EuiButtonIcon, - EuiFieldText, - EuiFlexGrid, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLoadingContent, - EuiPopover, - EuiText, - EuiToolTip, -} from '@elastic/eui' -import cx from 'classnames' -import { isNull } from 'lodash' -import React, { ChangeEvent, useEffect, useRef, useState } from 'react' -import { useSelector } from 'react-redux' -import AutoSizer from 'react-virtualized-auto-sizer' - -import { GroupBadge } from 'uiSrc/components' -import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' -import { - KEY_TYPES_ACTIONS, - KeyTypes, - LENGTH_NAMING_BY_TYPE, - ModulesKeyTypes, - STREAM_ADD_ACTION, - TEXT_DISABLED_COMPRESSED_VALUE, - TEXT_DISABLED_FORMATTER_EDITING, - TEXT_UNPRINTABLE_CHARACTERS, - TEXT_DISABLED_STRING_EDITING, -} from 'uiSrc/constants' -import { AddCommonFieldsFormConfig } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' -import { initialKeyInfo, keysSelector, selectedKeyDataSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys' -import { streamSelector } from 'uiSrc/slices/browser/stream' -import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { - formatBytes, - formatLongName, - isEqualBuffers, - isFormatEditable, - isFullStringLoaded, - MAX_TTL_NUMBER, - replaceSpaces, - stringToBuffer, - validateTTLNumber -} from 'uiSrc/utils' -import { stringDataSelector, stringSelector } from 'uiSrc/slices/browser/string' -import KeyValueFormatter from './components/Formatter' -import AutoRefresh from '../auto-refresh' - -import styles from './styles.module.scss' - -export interface Props { - keyType: KeyTypes | ModulesKeyTypes - onClose: (key: RedisResponseBuffer) => void - onRefresh: (key: RedisResponseBuffer, type: KeyTypes | ModulesKeyTypes) => void - onDelete: (key: RedisResponseBuffer, type: string) => void - onEditTTL: (key: RedisResponseBuffer, ttl: number) => void - onEditKey: (key: RedisResponseBuffer, newKey: RedisResponseBuffer, onFailure?: () => void) => void - onAddItem?: () => void - onEditItem?: () => void - onRemoveItem?: () => void - isFullScreen: boolean - arePanelsCollapsed: boolean - onToggleFullScreen: () => void -} - -const COPY_KEY_NAME_ICON = 'copyKeyNameIcon' - -const PADDING_WRAPPER_SIZE = 36 -const HIDE_LAST_REFRESH = 850 - PADDING_WRAPPER_SIZE -export const MIDDLE_SCREEN_RESOLUTION = 740 - PADDING_WRAPPER_SIZE - -const KeyDetailsHeader = ({ - isFullScreen, - arePanelsCollapsed, - onToggleFullScreen = () => {}, - onRefresh, - onClose, - onDelete, - onEditTTL, - onEditKey, - keyType, - onAddItem = () => {}, - onEditItem = () => {}, - onRemoveItem = () => {}, -}: Props) => { - const { loading, lastRefreshTime } = useSelector(selectedKeySelector) - const { - ttl: ttlProp, - type, - size, - length, - nameString: keyProp, - name: keyBuffer, - } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo - const { value: keyValue } = useSelector(stringDataSelector) - const { id: instanceId } = useSelector(connectedInstanceSelector) - const { isCompressed: isStringCompressed } = useSelector(stringSelector) - const { viewType } = useSelector(keysSelector) - const { viewType: streamViewType } = useSelector(streamSelector) - const { viewFormat: viewFormatProp } = useSelector(selectedKeySelector) - - const [isPopoverDeleteOpen, setIsPopoverDeleteOpen] = useState(false) - - const [ttl, setTTL] = useState(`${ttlProp}`) - const [ttlIsEditing, setTTLIsEditing] = useState(false) - const [ttlIsHovering, setTTLIsHovering] = useState(false) - - const [key, setKey] = useState(keyProp) - const [keyIsEditing, setKeyIsEditing] = useState(false) - const [keyIsHovering, setKeyIsHovering] = useState(false) - const [keyIsEditable, setKeyIsEditable] = useState(true) - - useEffect(() => { - setKey(keyProp) - setTTL(`${ttlProp}`) - setKeyIsEditable(isEqualBuffers(keyBuffer, stringToBuffer(keyProp || ''))) - }, [keyProp, ttlProp, keyBuffer]) - - const keyNameRef = useRef(null) - - const tooltipContent = formatLongName(keyProp || '') - - const onMouseEnterKey = () => { - setKeyIsHovering(true) - } - - const onMouseLeaveKey = () => { - setKeyIsHovering(false) - } - - const onClickKey = () => { - setKeyIsEditing(true) - } - - const onChangeKey = ({ currentTarget: { value } }: ChangeEvent) => { - keyIsEditing && setKey(value) - } - - const applyEditKey = () => { - setKeyIsEditing(false) - setKeyIsHovering(false) - - const newKeyBuffer = stringToBuffer(key || '') - - if (keyBuffer && !isEqualBuffers(keyBuffer, newKeyBuffer) && !isNull(keyProp)) { - onEditKey(keyBuffer, newKeyBuffer, () => setKey(keyProp)) - } - } - - const cancelEditKey = (event?: React.MouseEvent) => { - const { id } = event?.target as HTMLElement || {} - if (id === COPY_KEY_NAME_ICON) { - return - } - setKey(keyProp) - setKeyIsEditing(false) - setKeyIsHovering(false) - - event?.stopPropagation() - } - - const closePopoverDelete = () => { - setIsPopoverDeleteOpen(false) - } - - const showPopoverDelete = () => { - setIsPopoverDeleteOpen((isPopoverDeleteOpen) => !isPopoverDeleteOpen) - sendEventTelemetry({ - event: getBasedOnViewTypeEvent( - viewType, - TelemetryEvent.BROWSER_KEY_DELETE_CLICKED, - TelemetryEvent.TREE_VIEW_KEY_DELETE_CLICKED - ), - eventData: { - databaseId: instanceId, - source: 'keyValue', - keyType: type - } - }) - } - - const handleCopy = ( - event: any, - text = '', - keyInputIsEditing: boolean, - keyNameInputRef: React.RefObject - ) => { - navigator.clipboard.writeText(text) - - if (keyInputIsEditing) { - keyNameInputRef?.current?.focus() - } - - event.stopPropagation() - - sendEventTelemetry({ - event: getBasedOnViewTypeEvent( - viewType, - TelemetryEvent.BROWSER_KEY_COPIED, - TelemetryEvent.TREE_VIEW_KEY_COPIED - ), - eventData: { - databaseId: instanceId, - keyType: type - } - }) - } - - const handleRefreshKey = () => { - onRefresh(keyBuffer, type) - } - - const handleEnableAutoRefresh = (enableAutoRefresh: boolean, refreshRate: string) => { - const browserViewEvent = enableAutoRefresh - ? TelemetryEvent.BROWSER_KEY_DETAILS_AUTO_REFRESH_ENABLED - : TelemetryEvent.BROWSER_KEY_DETAILS_AUTO_REFRESH_DISABLED - const treeViewEvent = enableAutoRefresh - ? TelemetryEvent.TREE_VIEW_KEY_DETAILS_AUTO_REFRESH_ENABLED - : TelemetryEvent.TREE_VIEW_KEY_DETAILS_AUTO_REFRESH_DISABLED - sendEventTelemetry({ - event: getBasedOnViewTypeEvent(viewType, browserViewEvent, treeViewEvent), - eventData: { - length, - databaseId: instanceId, - keyType: type, - refreshRate: +refreshRate - } - }) - } - - const handleChangeAutoRefreshRate = (enableAutoRefresh: boolean, refreshRate: string) => { - if (enableAutoRefresh) { - handleEnableAutoRefresh(enableAutoRefresh, refreshRate) - } - } - - const onMouseEnterTTL = () => { - setTTLIsHovering(true) - } - - const onMouseLeaveTTL = () => { - setTTLIsHovering(false) - } - - const onClickTTL = () => { - setTTLIsEditing(true) - } - - const onChangeTtl = ({ currentTarget: { value } }: ChangeEvent) => { - ttlIsEditing && setTTL(validateTTLNumber(value) || '-1') - } - - const applyEditTTL = () => { - const ttlValue = ttl || '-1' - - setTTLIsEditing(false) - setTTLIsHovering(false) - - if (`${ttlProp}` !== ttlValue) { - onEditTTL(keyProp, +ttlValue) - } - } - - const cancelEditTTl = (event: any) => { - setTTL(`${ttlProp}`) - setTTLIsEditing(false) - setTTLIsHovering(false) - - event?.stopPropagation() - } - - const appendKeyEditing = () => - (!keyIsEditing ? : '') - - const appendTTLEditing = () => - (!ttlIsEditing ? : '') - - const KeySize = (width: number) => ( - - - - {formatBytes(size, 3)} - - )} - > - <> - {width > MIDDLE_SCREEN_RESOLUTION && 'Key Size: '} - {formatBytes(size, 0)} - - - - - ) - - const Actions = (width: number) => { - const isEditable = !isStringCompressed && isFormatEditable(viewFormatProp) - const isStringEditable = keyType === KeyTypes.String ? isFullStringLoaded(keyValue?.data?.length, length) : true - const noEditableText = isStringCompressed ? TEXT_DISABLED_COMPRESSED_VALUE : TEXT_DISABLED_FORMATTER_EDITING - const editToolTip = !isEditable ? noEditableText : (!isStringEditable ? TEXT_DISABLED_STRING_EDITING : null) - - return ( - <> - {KEY_TYPES_ACTIONS[keyType] && 'addItems' in KEY_TYPES_ACTIONS[keyType] && ( - MIDDLE_SCREEN_RESOLUTION ? '' : KEY_TYPES_ACTIONS[keyType].addItems?.name} - position="left" - anchorClassName={cx(styles.actionBtn, { [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION })} - > - <> - {width > MIDDLE_SCREEN_RESOLUTION ? ( - - {KEY_TYPES_ACTIONS[keyType].addItems?.name} - - ) : ( - - )} - - - )} - {keyType === KeyTypes.Stream && ( - MIDDLE_SCREEN_RESOLUTION ? '' : STREAM_ADD_ACTION[streamViewType].name} - position="left" - anchorClassName={cx(styles.actionBtn, { [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION })} - > - <> - {width > MIDDLE_SCREEN_RESOLUTION ? ( - - {STREAM_ADD_ACTION[streamViewType].name} - - ) : ( - - )} - - - )} - {KEY_TYPES_ACTIONS[keyType] && 'removeItems' in KEY_TYPES_ACTIONS[keyType] && ( - - - - )} - {KEY_TYPES_ACTIONS[keyType] && 'editItem' in KEY_TYPES_ACTIONS[keyType] && ( -
- - - -
- )} - - ) - } - - return ( -
- {loading ? ( -
- -
- ) : ( - - {({ width }) => ( -
- - - - - - {(keyIsEditing || keyIsHovering) && ( - - - - <> - applyEditKey()} - isDisabled={!keyIsEditable} - disabledTooltipText={TEXT_UNPRINTABLE_CHARACTERS} - onDecline={(event) => cancelEditKey(event)} - viewChildrenMode={!keyIsEditing} - isLoading={loading} - declineOnUnmount={false} - > - - -

{key}

- -
- {keyIsHovering && ( - - - handleCopy(event, key, keyIsEditing, keyNameRef)} - data-testid="copy-key-name-btn" - /> - - )} -
-
- )} - - - {replaceSpaces(keyProp?.substring(0, 200))} - - -
- - {!arePanelsCollapsed && ( - - - - - - )} - - {(!arePanelsCollapsed || isFullScreen) && ( - - onClose(keyProp)} - data-testid="close-key-btn" - /> - - )} - - -
- - {size && KeySize(width)} - - - {LENGTH_NAMING_BY_TYPE[type] ?? 'Length'} - {': '} - {length ?? '-'} - - - - <> - {(ttlIsEditing || ttlIsHovering) && ( - - - - TTL: - - - - applyEditTTL()} - onDecline={(event) => cancelEditTTl(event)} - viewChildrenMode={!ttlIsEditing} - isLoading={loading} - declineOnUnmount={false} - > - - - - - )} - - TTL: - - {ttl === '-1' ? 'No limit' : ttl} - - - - - -
- HIDE_LAST_REFRESH} - containerClassName={styles.actionBtn} - onRefresh={handleRefreshKey} - onEnableAutoRefresh={handleEnableAutoRefresh} - onChangeAutoRefreshRate={handleChangeAutoRefreshRate} - testid="refresh-key-btn" - /> - {Object.values(KeyTypes).includes(keyType) && } - {keyType && Actions(width)} - - - )} - > -
- -

- {tooltipContent} -

- - will be deleted. - -
-
- onDelete(keyBuffer, type)} - className={styles.popoverDeleteBtn} - data-testid="delete-key-confirm-btn" - > - Delete - -
-
-
-
-
-
-
- )} -
- )} -
- ) -} - -export default KeyDetailsHeader diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/index.ts b/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/index.ts deleted file mode 100644 index 744ca76be6..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import KeyValueFormatter from './KeyValueFormatter' - -export default KeyValueFormatter diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-details-header/styles.module.scss deleted file mode 100644 index 06a6c5a654..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/styles.module.scss +++ /dev/null @@ -1,221 +0,0 @@ -:global { - .browserPage { - .key-details-header { - .euiFieldText--compressed, - .euiFormControlLayout--compressed { - height: 29px !important; - } - - .euiFormControlLayout { - width: 100%; - max-width: 100%; - - &.euiFormControlLayout--readOnly { - border: 1px solid var(--controlsBorderColor); - cursor: auto; - } - - input { - height: 29px !important; - cursor: pointer; - max-width: none; - font-family: 'Graphik', sans-serif !important; - } - } - } - } -} - -:global(.browserPage .key-details-header .euiFormControlLayout) { - .keyInputEditing { - height: 31px !important; - } -} - -.container { - min-height: 108px; - padding: 18px 18px 12px 18px; - border-bottom: 1px solid var(--euiColorLightShade); - min-width: 100%; - position: relative; -} - -.key { - display: flex; - width: 100%; - min-width: 100%; - - padding-left: 9px; - line-height: 31px !important; -} - -.flexItemTTL { - width: 152px; - min-width: 152px; -} - -.ttlInput { - min-width: 106px; - font-size: 13px !important; - &.editing { - width: 124px; - } -} - -.keyInput { - height: 31px !important; - font-size: 14px !important; - font-weight: 500 !important; -} - -.ttlGridComponent, -.classNameGridComponent { - position: relative; -} - -.subtitleText { - padding: 6px 2px 6px 0; -} - -.hidden { - display: none; -} - -.ttlTextValue { - padding-left: 11px; -} - - -.controlsTTL, -.controlsKey { - position: absolute; - background-color: var(--euiColorLightestShade); - margin-top: 30px; - right: 0; - width: 80px; - height: 33px; - border-radius: 0 0 10px 10px; - - z-index: 1; - - :global(.euiButtonIcon) { - width: 50% !important; - height: 100% !important; - } -} - -.controlsKey { - right: 25px; -} - -.keyFlexItem { - overflow: hidden; -} - -.keyFlexItemEditing { - overflow: inherit; -} - -.cancelEditBtn:hover { - color: var(--euiColorColorDanger) !important; -} - -.applyEditBtn:hover { - color: var(--euiColorSecondary) !important; -} - -.popoverDeleteContainer { - overflow: hidden; - max-width: 350px !important; -} - -.popoverFooter { - margin-top: 10px; -} - -.closeBtn { - padding-top: 0 !important; - - svg { - width: 20px; - height: 20px; - } -} - -.backBtn:global(.euiButton.euiButton--fill.euiButton--secondary) { - background-color: var(--browserComponentActive) !important; - border-color: var(--browserComponentActive) !important; - color: var(--buttonSecondaryTextColor) !important; - - &:hover, &:focus { - color: var(--buttonSecondaryTextColor) !important; - } -} - -.flexItemKeyInput { - flex-direction: row !important; - width: 100% !important; -} - -.copyKey { - position: absolute; - padding-left: 7px; - padding-top: 4px; - right: 0; - height: 31px; - width: 25px; -} - -.subtitleActionBtns { - display: flex; - justify-content: flex-end; - align-items: center; - right: 13px; -} - -.toolTipAnchorKey { - max-width: calc(100% - 25px); - height: 31px !important; -} - -.keyHiddenText { - display: inline-block; - visibility: hidden; - height: 1px; - overflow: hidden; - max-width: 100%; - margin-right: 80px; - word-break: break-all; -} - -.actionBtn { - margin-right: 12px; - position: relative; - z-index: 2; - - &.withText { - color: var(--euiTextSubduedColor) !important; - :global(.euiButton__text) { - font: normal normal normal 12px/18px Graphik !important; - } - } -} - -.capitalize { - text-transform: capitalize; -} - -.groupSecondLine { - margin-top: 4px !important; -} - -.refreshSummary { - color: var(--euiColorMediumShade) !important; - font: normal normal normal 12px/18px Graphik, sans-serif; - padding-bottom: 2px; - margin-right: 4px; -} - -.refreshTime { - color: var(--euiTextSubduedColor) !important; -} diff --git a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx deleted file mode 100644 index 7ecd333ee0..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { - EuiText, - EuiFlexGroup, - EuiButtonIcon, - EuiToolTip, -} from '@elastic/eui' -import { isNull } from 'lodash' -import { useDispatch, useSelector } from 'react-redux' -import cx from 'classnames' -import { - AddStreamEntries, - AddListElements, - AddSetMembers, - AddZsetMembers, - AddHashFields, - AddStreamGroup -} from 'uiSrc/pages/browser/components/key-details-add-items' - -import { - selectedKeyDataSelector, - selectedKeySelector, - keysSelector, -} from 'uiSrc/slices/browser/keys' -import { cleanRangeFilter, streamSelector } from 'uiSrc/slices/browser/stream' -import { KeyTypes, ModulesKeyTypes, MODULES_KEY_TYPES_NAMES, STREAM_ADD_GROUP_VIEW_TYPES } from 'uiSrc/constants' -import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { StreamViewType } from 'uiSrc/slices/interfaces/stream' -import { sendEventTelemetry, TelemetryEvent, getBasedOnViewTypeEvent } from 'uiSrc/telemetry' -import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' - -import ExploreGuides from 'uiSrc/components/explore-guides' -import { Nullable } from 'uiSrc/utils' -import { IFetchKeyArgs } from 'uiSrc/constants/prop-types/keys' -import KeyDetailsHeader from '../../key-details-header/KeyDetailsHeader' -import ZSetDetails from '../../zset-details/ZSetDetails' -import StringDetails from '../../string-details/StringDetails' -import SetDetails from '../../set-details/SetDetails' -import HashDetails from '../../hash-details/HashDetails' -import ListDetails from '../../list-details/ListDetails' -import RejsonDetailsWrapper from '../../rejson-details/RejsonDetailsWrapper' -import StreamDetailsWrapper from '../../stream-details' -import RemoveListElements from '../../key-details-remove-items/remove-list-elements/RemoveListElements' -import UnsupportedTypeDetails from '../../unsupported-type-details/UnsupportedTypeDetails' -import ModulesTypeDetails from '../../modules-type-details/ModulesTypeDetails' - -import styles from '../styles.module.scss' - -export interface Props { - isFullScreen: boolean - arePanelsCollapsed: boolean - onToggleFullScreen: () => void - onClose: (key: RedisResponseBuffer) => void - onClosePanel: () => void - onRefresh: (key: RedisResponseBuffer, type: KeyTypes | ModulesKeyTypes, args: IFetchKeyArgs) => void - onDelete: (key: RedisResponseBuffer, type: string) => void - onRemoveKey: () => void - onEditTTL: (key: RedisResponseBuffer, ttl: number) => void - onEditKey: (key: RedisResponseBuffer, newKey: RedisResponseBuffer, onFailure?: () => void) => void - totalKeys: number - keysLastRefreshTime: Nullable -} - -const KeyDetails = ({ ...props }: Props) => { - const { onRefresh, onClosePanel, onRemoveKey, totalKeys, keysLastRefreshTime } = props - const { loading, error = '', data } = useSelector(selectedKeySelector) - const { type: selectedKeyType, name: selectedKey } = useSelector(selectedKeyDataSelector) ?? { - type: KeyTypes.String, - } - const isKeySelected = !isNull(useSelector(selectedKeyDataSelector)) - const { id: instanceId } = useSelector(connectedInstanceSelector) - const { viewType } = useSelector(keysSelector) - const { viewType: streamViewType } = useSelector(streamSelector) - const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState(false) - const [isRemoveItemPanelOpen, setIsRemoveItemPanelOpen] = useState(false) - const [editItem, setEditItem] = useState(false) - - const dispatch = useDispatch() - - useEffect(() => { - // Close 'Add Item Panel' and remove stream range on change selected key - closeAddItemPanel() - dispatch(cleanRangeFilter()) - }, [selectedKey]) - - const openAddItemPanel = () => { - setIsRemoveItemPanelOpen(false) - setIsAddItemPanelOpen(true) - if (!STREAM_ADD_GROUP_VIEW_TYPES.includes(streamViewType)) { - sendEventTelemetry({ - event: getBasedOnViewTypeEvent( - viewType, - TelemetryEvent.BROWSER_KEY_ADD_VALUE_CLICKED, - TelemetryEvent.TREE_VIEW_KEY_ADD_VALUE_CLICKED - ), - eventData: { - databaseId: instanceId, - keyType: selectedKeyType - } - }) - } - } - - const openRemoveItemPanel = () => { - setIsAddItemPanelOpen(false) - setIsRemoveItemPanelOpen(true) - } - - const closeAddItemPanel = (isCancelled?: boolean) => { - if (isCancelled && isAddItemPanelOpen && !STREAM_ADD_GROUP_VIEW_TYPES.includes(streamViewType)) { - sendEventTelemetry({ - event: getBasedOnViewTypeEvent( - viewType, - TelemetryEvent.BROWSER_KEY_ADD_VALUE_CANCELLED, - TelemetryEvent.TREE_VIEW_KEY_ADD_VALUE_CANCELLED - ), - eventData: { - databaseId: instanceId, - keyType: selectedKeyType - } - }) - } - setIsAddItemPanelOpen(false) - } - - const closeRemoveItemPanel = () => { - setIsRemoveItemPanelOpen(false) - } - - const TypeDetails: any = { - [KeyTypes.ZSet]: , - [KeyTypes.Set]: , - [KeyTypes.String]: ( - setEditItem(isEdit)} - onRefresh={onRefresh} - /> - ), - [KeyTypes.Hash]: , - [KeyTypes.List]: , - [KeyTypes.ReJSON]: , - [KeyTypes.Stream]: , - } - - const NoKeysSelectedMessage = () => ( - <> - {totalKeys > 0 ? ( - - Select the key from the list on the left to see the details of the key. - - ) : ()} - - ) - - return ( -
- - <> - {!isKeySelected && !loading ? ( - <> - - - - -
- - {error ? ( -

- {error} -

- ) : (!!keysLastRefreshTime && )} -
-
- - ) : ( -
- setEditItem(!editItem)} - keyType={selectedKeyType} - {...props} - /> -
- {!loading && ( -
- {(selectedKeyType && selectedKeyType in TypeDetails) && TypeDetails[selectedKeyType]} - - {(Object.values(ModulesKeyTypes).includes(selectedKeyType)) && ( - - )} - - {!(Object.values(KeyTypes).includes(selectedKeyType)) - && !(Object.values(ModulesKeyTypes).includes(selectedKeyType)) && ( - - )} -
- )} - {isAddItemPanelOpen && ( -
- {selectedKeyType === KeyTypes.Hash && ( - - )} - {selectedKeyType === KeyTypes.ZSet && ( - - )} - {selectedKeyType === KeyTypes.Set && ( - - )} - {selectedKeyType === KeyTypes.List && ( - - )} - {selectedKeyType === KeyTypes.Stream && ( - <> - {streamViewType === StreamViewType.Data && ( - - )} - {STREAM_ADD_GROUP_VIEW_TYPES.includes(streamViewType) && ( - - )} - - )} -
- )} - {isRemoveItemPanelOpen && ( -
- {selectedKeyType === KeyTypes.List && ( - - )} -
- )} -
-
- )} - -
-
- ) -} - -export default KeyDetails diff --git a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.spec.tsx deleted file mode 100644 index f601145b75..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.spec.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react' -import { instance, mock } from 'ts-mockito' -import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' -import { KeyTypes } from 'uiSrc/constants' - -import KeyDetails, { Props as KeyDetailsProps } from './KeyDetails/KeyDetails' -import KeyDetailsWrapper, { Props } from './KeyDetailsWrapper' - -const mockedProps = mock() -const key = 'key' - -interface ExtendedKeyDetailsProps extends KeyDetailsProps { - keyType: string -} - -const MockKeyDetails = (props: ExtendedKeyDetailsProps) => ( -
- - - - - -
-) - -jest.mock('./KeyDetails/KeyDetails', () => ({ - __esModule: true, - namedExport: jest.fn(), - default: jest.fn(), -})) - -// jest.mock('uiSrc/slices/browser/hash') -// jest.mock('uiSrc/slices/browser/zset') -// jest.mock('uiSrc/slices/browser/string') -// jest.mock('uiSrc/slices/browser/set') -// jest.mock('uiSrc/slices/browser/list') -// jest.mock('uiSrc/slices/browser/keys') - -describe('KeyDetailsWrapper', () => { - beforeAll(() => { - KeyDetails.mockImplementation(MockKeyDetails) - }) - // beforeEach(() => { - // refreshHashFieldsAction.mockImplementation(() => jest.fn) - // refreshZsetMembersAction.mockImplementation(() => jest.fn) - // resetStringValue.mockImplementation(() => jest.fn) - // refreshSetMembersAction.mockImplementation(() => jest.fn) - // refreshListElementsAction.mockImplementation(() => jest.fn) - // deleteKeyAction.mockImplementation(() => jest.fn) - // editKey.mockImplementation(() => jest.fn) - // editKeyTTL.mockImplementation(() => jest.fn) - // fetchKeyInfo.mockImplementation(() => jest.fn) - // refreshKeyInfoAction.mockImplementation(() => jest.fn) - // selectedKeySelector.mockReturnValue('keyName') - // }) - it('should render', () => { - expect( - render() - ).toBeTruthy() - }) - - describe('should call onRefresh', () => { - test.each(Object.values(KeyTypes))('should call onRefresh', (keyType) => { - KeyDetails.mockImplementationOnce((props: KeyDetailsProps) => ( - - )) - const component = render() - fireEvent.click(screen.getByTestId('refresh-btn')) - expect(component).toBeTruthy() - }) - }) - - it('should call onDelete', () => { - const component = render() - fireEvent.click(screen.getByTestId('delete-btn')) - expect(component).toBeTruthy() - }) - - it('should call onClose', () => { - const onClose = jest.fn() - const component = render() - fireEvent.click(screen.getByTestId('close-btn')) - expect(component).toBeTruthy() - expect(onClose).toBeCalled() - }) - - it('should call onEditKey', () => { - const component = render() - fireEvent.click(screen.getByTestId('edit-key-btn')) - expect(component).toBeTruthy() - }) - - it('should call onEditTtl', () => { - const component = render() - fireEvent.click(screen.getByTestId('edit-ttl-btn')) - expect(component).toBeTruthy() - }) -}) diff --git a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx b/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx deleted file mode 100644 index 91da72d0ee..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetailsWrapper.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React, { useEffect } from 'react' -import { isUndefined } from 'lodash' -import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' - -import { - deleteSelectedKeyAction, - editKey, - editKeyTTL, - fetchKeyInfo, - keysSelector, - refreshKeyInfoAction, - selectedKeyDataSelector, - toggleBrowserFullScreen, -} from 'uiSrc/slices/browser/keys' -import { KeyTypes, ModulesKeyTypes, STRING_MAX_LENGTH } from 'uiSrc/constants' -import { refreshHashFieldsAction } from 'uiSrc/slices/browser/hash' -import { refreshZsetMembersAction } from 'uiSrc/slices/browser/zset' -import { fetchString, resetStringValue } from 'uiSrc/slices/browser/string' -import { refreshSetMembersAction } from 'uiSrc/slices/browser/set' -import { refreshListElementsAction } from 'uiSrc/slices/browser/list' -import { fetchReJSON } from 'uiSrc/slices/browser/rejson' -import { refreshStream } from 'uiSrc/slices/browser/stream' -import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { Nullable } from 'uiSrc/utils' -import { IFetchKeyArgs } from 'uiSrc/constants/prop-types/keys' -import KeyDetails from './KeyDetails/KeyDetails' - -export interface Props { - isFullScreen: boolean - arePanelsCollapsed: boolean - onToggleFullScreen: () => void - onCloseKey: () => void - onEditKey: (key: RedisResponseBuffer, newKey: RedisResponseBuffer) => void - onRemoveKey: () => void - keyProp: RedisResponseBuffer | null - totalKeys: number - keysLastRefreshTime: Nullable -} - -const KeyDetailsWrapper = (props: Props) => { - const { - isFullScreen, - arePanelsCollapsed, - onToggleFullScreen, - onCloseKey, - onEditKey, - onRemoveKey, - keyProp, - totalKeys, - keysLastRefreshTime, - } = props - - const { instanceId } = useParams<{ instanceId: string }>() - const { viewType } = useSelector(keysSelector) - const { type: keyType, name: keyName, length: keyLength } = useSelector(selectedKeyDataSelector) ?? { - type: KeyTypes.String, - } - - const dispatch = useDispatch() - - useEffect(() => { - if (keyProp === null) { - return - } - // Restore key details from context in future - // (selectedKey.data?.name !== keyProp) - dispatch(fetchKeyInfo(keyProp)) - }, [keyProp]) - - useEffect(() => { - if (!isUndefined(keyName)) { - sendEventTelemetry({ - event: getBasedOnViewTypeEvent( - viewType, - TelemetryEvent.BROWSER_KEY_VALUE_VIEWED, - TelemetryEvent.TREE_VIEW_KEY_VALUE_VIEWED - ), - eventData: { - keyType, - databaseId: instanceId, - length: keyLength, - } - }) - } - }, [keyName]) - - const handleDeleteKey = (key: RedisResponseBuffer, type: string) => { - dispatch(deleteSelectedKeyAction(key, - () => { - if (type === KeyTypes.String) { - dispatch(resetStringValue()) - } - onRemoveKey() - })) - } - - const handleRefreshKey = (key: RedisResponseBuffer, type: KeyTypes | ModulesKeyTypes, args: IFetchKeyArgs) => { - const resetData = false - dispatch(refreshKeyInfoAction(key)) - switch (type) { - case KeyTypes.Hash: { - dispatch(refreshHashFieldsAction(key, resetData)) - break - } - case KeyTypes.ZSet: { - dispatch(refreshZsetMembersAction(key, resetData)) - break - } - case KeyTypes.Set: { - dispatch(refreshSetMembersAction(key, resetData)) - break - } - case KeyTypes.List: { - dispatch(refreshListElementsAction(key, resetData)) - break - } - case KeyTypes.String: { - dispatch(fetchString(key, { resetData, end: args?.end || STRING_MAX_LENGTH })) - break - } - case KeyTypes.ReJSON: { - dispatch(fetchReJSON(key, '.', true)) - break - } - case KeyTypes.Stream: { - dispatch(refreshStream(key, resetData)) - break - } - default: - dispatch(fetchKeyInfo(key, resetData)) - } - } - - const handleEditTTL = (key: RedisResponseBuffer, ttl: number) => { - dispatch(editKeyTTL(key, ttl)) - } - const handleEditKey = (oldKey: RedisResponseBuffer, newKey: RedisResponseBuffer, onFailure?: () => void) => { - dispatch(editKey(oldKey, newKey, () => onEditKey(oldKey, newKey), onFailure)) - } - - const handleClose = () => { - onCloseKey() - } - - const handleClosePanel = () => { - dispatch(toggleBrowserFullScreen(true)) - keyProp && onCloseKey() - } - - return ( - - ) -} - -export default React.memo(KeyDetailsWrapper) diff --git a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx index 4407f212bf..6df981124a 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx @@ -4,34 +4,13 @@ import cx from 'classnames' import { useParams } from 'react-router-dom' import { debounce, findIndex, isUndefined, reject } from 'lodash' -import { - EuiText, - EuiToolTip, - EuiTextColor, - EuiLoadingContent, - EuiPopover, - EuiButton, - EuiButtonIcon, EuiSpacer, -} from '@elastic/eui' import { CellMeasurerCache } from 'react-virtualized' import { - formatBytes, - truncateNumberToDuration, - truncateNumberToFirstUnit, - truncateTTLToSeconds, - replaceSpaces, - formatLongName, bufferToString, bufferFormatRangeItems, Nullable, Maybe, } from 'uiSrc/utils' -import { - NoResultsFoundText, - FullScanNoResultsFoundText, - ScanNoResultsFoundText, - NoSelectedIndexText, -} from 'uiSrc/constants/texts' import { deleteKeyAction, fetchKeysMetadata, @@ -46,20 +25,22 @@ import { setBrowserIsNotRendered, setBrowserRedisearchScrollPosition, } from 'uiSrc/slices/app/context' -import { GroupBadge } from 'uiSrc/components' import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import { KeysStoreData, SearchMode } from 'uiSrc/slices/interfaces/keys' import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' import { KeyTypes, ModulesKeyTypes, TableCellAlignment, TableCellTextAlignment } from 'uiSrc/constants' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' -import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { redisearchSelector } from 'uiSrc/slices/browser/redisearch' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import NoKeysFound from 'uiSrc/pages/browser/components/no-keys-found' +import KeyRowTTL from 'uiSrc/pages/browser/components/key-row-ttl' +import KeyRowSize from 'uiSrc/pages/browser/components/key-row-size' +import KeyRowName from 'uiSrc/pages/browser/components/key-row-name' +import KeyRowType from 'uiSrc/pages/browser/components/key-row-type' import { GetKeyInfoResponse } from 'apiSrc/modules/browser/dto' +import NoKeysMessage from '../no-keys-message' import styles from './styles.module.scss' export interface Props { @@ -101,9 +82,8 @@ const KeyList = forwardRef((props: Props, ref) => { const { instanceId = '' } = useParams<{ instanceId: string }>() const selectedKey = useSelector(selectedKeySelector) - const { total, nextCursor, previousResultCount } = useSelector(keysDataSelector) - const { isSearched, isFiltered, viewType, searchMode, deleting } = useSelector(keysSelector) - const { selectedIndex } = useSelector(redisearchSelector) + const { nextCursor, previousResultCount } = useSelector(keysDataSelector) + const { isSearched, isFiltered, searchMode, deleting } = useSelector(keysSelector) const { keyList: { isNotRendered: isNotRenderedContext } } = useSelector(appContextBrowser) const [, rerender] = useState({}) @@ -155,42 +135,25 @@ const KeyList = forwardRef((props: Props, ref) => { controller.current?.abort() } + const NoItemsMessage = () => ( + + ) + const getNoItemsMessage = () => { if (isNotRendered.current) { return '' } - if (searchMode === SearchMode.Redisearch) { - if (!selectedIndex) { - return NoSelectedIndexText - } - - if (total === 0) { - return NoResultsFoundText - } - - if (isSearched) { - return keysState.scanned < total ? NoResultsFoundText : FullScanNoResultsFoundText - } - } - - if (total === 0) { - return () - } - - if (isSearched) { - return keysState.scanned < total ? ScanNoResultsFoundText : FullScanNoResultsFoundText - } - - if (isFiltered && keysState.scanned < total) { - return ScanNoResultsFoundText - } - if (itemsRef.current.length < keysState.keys.length) { return 'loading...' } - return NoResultsFoundText + return } const onLoadMoreItems = (props: { startIndex: number, stopIndex: number }) => { @@ -223,11 +186,7 @@ const KeyList = forwardRef((props: Props, ref) => { const handleDeletePopoverOpen = (index: Maybe, type: KeyTypes | ModulesKeyTypes) => { if (index !== deletePopoverIndex) { sendEventTelemetry({ - event: getBasedOnViewTypeEvent( - viewType, - TelemetryEvent.BROWSER_KEY_DELETE_CLICKED, - TelemetryEvent.TREE_VIEW_KEY_DELETE_CLICKED - ), + event: TelemetryEvent.BROWSER_KEY_DELETE_CLICKED, eventData: { databaseId: instanceId, keyType: type, @@ -322,10 +281,8 @@ const KeyList = forwardRef((props: Props, ref) => { label: 'Type', absoluteWidth: 'auto', minWidth: 126, - render: (cellData: any, { nameString: name }: any) => ( - isUndefined(cellData) - ? - : + render: (cellData: any, { nameString }: any) => ( + ) }, { @@ -333,36 +290,9 @@ const KeyList = forwardRef((props: Props, ref) => { label: 'Key', minWidth: 94, truncateText: true, - render: (cellData: string) => { - if (isUndefined(cellData)) { - return ( - - ) - } - // Better to cut the long string, because it could affect virtual scroll performance - const name = cellData || '' - const cellContent = replaceSpaces(name?.substring(0, 200)) - const tooltipContent = formatLongName(name) - return ( - -
- - <>{cellContent} - -
-
- ) - } + render: (cellData: string) => ( + + ) }, { id: 'ttl', @@ -371,48 +301,9 @@ const KeyList = forwardRef((props: Props, ref) => { minWidth: 86, truncateText: true, alignment: TableCellAlignment.Right, - render: (cellData: number, { nameString: name }: IKeyPropTypes, _expanded, rowIndex) => { - if (isUndefined(cellData)) { - return - } - if (cellData === -1) { - return ( - - No limit - - ) - } - return ( - -
- - {`${truncateTTLToSeconds(cellData)} s`} -
- {`(${truncateNumberToDuration(cellData)})`} - - )} - > - <>{truncateNumberToFirstUnit(cellData)} -
-
-
- ) - }, + render: (cellData: number, { nameString }: IKeyPropTypes, _expanded, rowIndex) => ( + + ) }, { id: 'size', @@ -423,84 +314,23 @@ const KeyList = forwardRef((props: Props, ref) => { textAlignment: TableCellTextAlignment.Right, render: ( cellData: number, - { nameString: name, type, name: bufferName }: IKeyPropTypes, + { nameString, type, name: bufferName }: IKeyPropTypes, _expanded, rowIndex - ) => { - if (isUndefined(cellData)) { - return - } - - if (!cellData) { - return ( - - - - - ) - } - return ( - <> - -
- - {formatBytes(cellData, 3)} - - )} - > - <>{formatBytes(cellData, 0)} - -
-
- setDeletePopoverIndex(undefined)} - panelPaddingSize="l" - panelClassName={styles.deletePopover} - button={( - handleDeletePopoverOpen(rowIndex, type)} - aria-label="Delete Key" - data-testid={`delete-key-btn-${name}`} - /> - )} - onClick={(e) => e.stopPropagation()} - > - <> - -

{formatLongName(name)}

- will be deleted. -
- - handleRemoveKey(bufferName)} - data-testid="submit-delete-key" - > - Delete - - -
- - ) - } + ) => ( + + ) }, ] diff --git a/redisinsight/ui/src/pages/browser/components/key-list/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-list/styles.module.scss index 9b75dab2e9..ddefddc587 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/key-list/styles.module.scss @@ -27,19 +27,19 @@ :global { .ReactVirtualized__Table__row { .ReactVirtualized__Table__rowColumn { - .moveOnHover { + .moveOnHoverKey { transition: transform ease 0.3s; &.hide { transform: translateX(-8px) } } - .showOnHover { + .showOnHoverKey { display: none; &.show { display: block !important; }} } &:hover { .ReactVirtualized__Table__rowColumn { - .moveOnHover { transform: translateX(-8px) } - .showOnHover { display: block; } + .moveOnHoverKey { transform: translateX(-8px) } + .showOnHoverKey { display: block !important; } } } } diff --git a/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx new file mode 100644 index 0000000000..8ee2f086ea --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' + +import KeyRowName, { Props } from './KeyRowName' + +const mockedProps = mock() + +const loadingTestId = 'name-loading' + +describe('KeyRowName', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render Loading if no nameString', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId)).toBeInTheDocument() + }) + + it('content should be no more than 200 symbols', () => { + const longName = Array.from({ length: 250 }, () => '1').join('') + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId)).not.toBeInTheDocument() + expect(queryByTestId(`key-${longName}`)).toHaveTextContent(longName.slice(0, 200)) + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx new file mode 100644 index 0000000000..5dfc1df372 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { + EuiLoadingContent, + EuiText, + EuiToolTip, +} from '@elastic/eui' +import { isUndefined } from 'lodash' + +import { Maybe, formatLongName, replaceSpaces } from 'uiSrc/utils' +import styles from './styles.module.scss' + +export interface Props { + nameString: Maybe +} + +const KeyRowName = (props: Props) => { + const { nameString } = props + + if (isUndefined(nameString)) { + return ( + + ) + } + + // Better to cut the long string, because it could affect virtual scroll performance + const nameContent = replaceSpaces(nameString?.substring?.(0, 200)) + const nameTooltipContent = formatLongName(nameString) + + return ( +
+ +
+ + <>{nameContent} + +
+
+
+ ) +} + +export default KeyRowName diff --git a/redisinsight/ui/src/pages/browser/components/key-row-name/index.ts b/redisinsight/ui/src/pages/browser/components/key-row-name/index.ts new file mode 100644 index 0000000000..03ecaaf22a --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/index.ts @@ -0,0 +1,3 @@ +import KeyRowName from './KeyRowName' + +export default KeyRowName diff --git a/redisinsight/ui/src/pages/browser/components/key-row-name/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-row-name/styles.module.scss new file mode 100644 index 0000000000..8157e074f4 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/styles.module.scss @@ -0,0 +1,15 @@ +.keyInfoLoading { + width: 70%; + margin-top: 7px; +} + +.keyName { + flex-grow: 1; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + + :global(.euiTextColor) { + max-width: 100%; + } +} diff --git a/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.spec.tsx new file mode 100644 index 0000000000..69e2da224c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' + +import { render } from 'uiSrc/utils/test-utils' +import { formatBytes } from 'uiSrc/utils' +import KeyRowSize, { Props } from './KeyRowSize' + +const mockedProps = mock() +const loadingTestId = 'size-loading_' +const nameString = 'name' + +describe('KeyRowSize', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render Loading if no size', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).toBeInTheDocument() + }) + + it('should render "-" if size is empty', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument() + expect(queryByTestId(`size-${nameString}`)).toHaveTextContent('-') + }) + + it('should render formatted size', () => { + const size = 123123123 + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument() + expect(queryByTestId(`size-${nameString}`)).toHaveTextContent(formatBytes(size, 0) as string) + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.tsx b/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.tsx new file mode 100644 index 0000000000..e0dce54212 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.tsx @@ -0,0 +1,135 @@ +import React from 'react' +import cx from 'classnames' +import { + EuiButton, + EuiButtonIcon, + EuiLoadingContent, + EuiPopover, + EuiSpacer, + EuiText, + EuiToolTip, +} from '@elastic/eui' +import { isUndefined } from 'lodash' + +import { Maybe, formatBytes, formatLongName } from 'uiSrc/utils' +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import styles from './styles.module.scss' + +export interface Props { + size: Maybe + deletePopoverId: Maybe + rowId: number | string + nameString: string + type: KeyTypes | ModulesKeyTypes + deleting: boolean + nameBuffer: RedisResponseBuffer + setDeletePopoverId: (id: any) => void + handleDeletePopoverOpen: (id: any, type: KeyTypes | ModulesKeyTypes) => void + handleDelete: (key: RedisResponseBuffer) => void +} + +const KeyRowSize = (props: Props) => { + const { + size, + nameString, + nameBuffer, + deletePopoverId, + deleting, + rowId, + type, + setDeletePopoverId, + handleDeletePopoverOpen, + handleDelete, + } = props + + if (isUndefined(size)) { + return ( + + ) + } + + if (!size) { + return ( + + - + + ) + } + return ( + <> + +
+ + {formatBytes(size, 3)} + + )} + > + <>{formatBytes(size, 0)} + +
+
+ setDeletePopoverId(undefined)} + panelPaddingSize="l" + panelClassName={styles.deletePopover} + button={( + handleDeletePopoverOpen(rowId, type)} + aria-label="Delete Key" + data-testid={`delete-key-btn-${nameString}`} + /> + )} + onClick={(e) => e.stopPropagation()} + > + <> + +

{formatLongName(nameString)}

+ will be deleted. +
+ + handleDelete(nameBuffer)} + data-testid="submit-delete-key" + > + Delete + + +
+ + ) +} + +export default KeyRowSize diff --git a/redisinsight/ui/src/pages/browser/components/key-row-size/index.ts b/redisinsight/ui/src/pages/browser/components/key-row-size/index.ts new file mode 100644 index 0000000000..f139af942d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-size/index.ts @@ -0,0 +1,3 @@ +import KeyRowSize from './KeyRowSize' + +export default KeyRowSize diff --git a/redisinsight/ui/src/pages/browser/components/key-row-size/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-row-size/styles.module.scss new file mode 100644 index 0000000000..a8cddcd317 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-size/styles.module.scss @@ -0,0 +1,10 @@ +.keyInfoLoading { + margin-top: 8px; + padding-left: 16px; +} + +.keySize { + width: 90px; + min-width: 90px; + text-align: right; +} diff --git a/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.spec.tsx new file mode 100644 index 0000000000..24a0d7ba8b --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' + +import { render } from 'uiSrc/utils/test-utils' +import { truncateNumberToFirstUnit } from 'uiSrc/utils' +import KeyRowTTL, { Props } from './KeyRowTTL' + +const mockedProps = mock() +const loadingTestId = 'ttl-loading_' +const nameString = 'name' + +describe('KeyRowTTL', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render Loading if no ttl', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).toBeInTheDocument() + }) + + it('should render "No limit" if ttl is -1', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument() + expect(queryByTestId(`ttl-${nameString}`)).toHaveTextContent('No limit') + }) + + it('should render formatted ttl', () => { + const ttl = 123123123 + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument() + expect(queryByTestId(`ttl-${nameString}`)).toHaveTextContent(truncateNumberToFirstUnit(ttl)) + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.tsx b/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.tsx new file mode 100644 index 0000000000..1c52434606 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import cx from 'classnames' +import { + EuiLoadingContent, + EuiText, + EuiTextColor, + EuiToolTip, +} from '@elastic/eui' +import { isUndefined } from 'lodash' + +import { + Maybe, + truncateNumberToDuration, + truncateNumberToFirstUnit, + truncateTTLToSeconds, +} from 'uiSrc/utils' +import styles from './styles.module.scss' + +export interface Props { + ttl: Maybe + deletePopoverId: Maybe + rowId: number | string + nameString: string +} + +const KeyRowTTL = (props: Props) => { + const { ttl, nameString, deletePopoverId, rowId } = props + + if (isUndefined(ttl)) { + return ( + + ) + } + if (ttl === -1) { + return ( + + No limit + + ) + } + return ( + +
+ + {`${truncateTTLToSeconds(ttl)} s`} +
+ {`(${truncateNumberToDuration(ttl)})`} + + )} + > + <>{truncateNumberToFirstUnit(ttl)} +
+
+
+ ) +} + +export default KeyRowTTL diff --git a/redisinsight/ui/src/pages/browser/components/key-row-ttl/index.ts b/redisinsight/ui/src/pages/browser/components/key-row-ttl/index.ts new file mode 100644 index 0000000000..bd31d8b0a6 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-ttl/index.ts @@ -0,0 +1,3 @@ +import KeyRowTTL from './KeyRowTTL' + +export default KeyRowTTL diff --git a/redisinsight/ui/src/pages/browser/components/key-row-ttl/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-row-ttl/styles.module.scss new file mode 100644 index 0000000000..d05353485f --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-ttl/styles.module.scss @@ -0,0 +1,10 @@ +.keyInfoLoading { + margin-top: 8px; + padding-left: 16px; +} + +.keyTTL { + width: 86px; + min-width: 86px; + text-align: right; +} diff --git a/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.spec.tsx new file mode 100644 index 0000000000..170f56aea4 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.spec.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' + +import { render } from 'uiSrc/utils/test-utils' +import { KeyTypes } from 'uiSrc/constants' +import KeyRowType, { Props } from './KeyRowType' + +const mockedProps = mock() +const loadingTestId = 'type-loading_' +const nameString = 'name' + +describe('KeyRowType', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render Loading if no type', () => { + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).toBeInTheDocument() + }) + + it('should render Badge if type exists', () => { + const type = KeyTypes.Hash + const { queryByTestId } = render() + + expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument() + expect(queryByTestId(`badge-${type}_${nameString}`)).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.tsx b/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.tsx new file mode 100644 index 0000000000..45d6e920aa --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import cx from 'classnames' +import { + EuiLoadingContent, +} from '@elastic/eui' + +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import { GroupBadge } from 'uiSrc/components' +import styles from './styles.module.scss' + +export interface Props { + nameString: string + type: KeyTypes | ModulesKeyTypes +} + +const KeyRowType = (props: Props) => { + const { nameString, type } = props + + return ( + <> + {!type && ( + + )} + {!!type && ( +
+ +
+ )} + + ) +} + +export default KeyRowType diff --git a/redisinsight/ui/src/pages/browser/components/key-row-type/index.ts b/redisinsight/ui/src/pages/browser/components/key-row-type/index.ts new file mode 100644 index 0000000000..772282a678 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-type/index.ts @@ -0,0 +1,3 @@ +import KeyRowType from './KeyRowType' + +export default KeyRowType diff --git a/redisinsight/ui/src/pages/browser/components/key-row-type/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-row-type/styles.module.scss new file mode 100644 index 0000000000..01ef8ef796 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-row-type/styles.module.scss @@ -0,0 +1,11 @@ +.keyInfoLoading { + margin-top: 8px; + padding-left: 16px; +} + +.keyType { + padding-right: 16px; + padding-left: 12px; + width: 126px; + min-width: 126px; +} diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx index a38b56f5f9..b4c35023d2 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx @@ -2,17 +2,15 @@ import { cloneDeep } from 'lodash' import React from 'react' import { cleanup, - clearStoreActions, fireEvent, mockedStore, render, - screen, - act, } from 'uiSrc/utils/test-utils' -import { setSearchMatch } from 'uiSrc/slices/browser/keys' import { KeysStoreData } from 'uiSrc/slices/interfaces/keys' -import { mockVirtualTreeResult } from 'uiSrc/components/virtual-tree/VirtualTree.spec' -import { setBrowserTreeNodesOpen, setBrowserTreeSelectedLeaf } from 'uiSrc/slices/app/context' +import { setBrowserTreeNodesOpen } from 'uiSrc/slices/app/context' +import { stringToBuffer } from 'uiSrc/utils' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { KeyTypes } from 'uiSrc/constants' import KeyTree from './KeyTree' let store: typeof mockedStore @@ -55,36 +53,58 @@ const propsMock = { lastRefreshTime: 3 } as KeysStoreData, loading: false, + deleting: false, + commonFilterType: null, selectKey: jest.fn(), + loadMoreItems: jest.fn(), + onDelete: jest.fn(), + onAddKeyPanel: jest.fn(), + onBulkActionsPanel: jest.fn(), } -const mockLeafKeys = { - test: { name: 'test', type: 'hash', ttl: -1, size: 9849176 } -} +const leafRootFullName = 'test' +const folderFullName = 'car:' +const leaf1FullName = 'car:110' +const leaf2FullName = 'car:210' const mockWebWorkerResult = [{ children: [{ children: [], - fullName: 'car:110:', - id: '0.snc1rc3zwgo', + fullName: leaf1FullName, + id: '0.0', + keyApproximate: 0.01, + keyCount: 1, + name: '110', + type: KeyTypes.String, + isLeaf: true, + nameBuffer: stringToBuffer(leaf1FullName), + }, { + children: [], + fullName: leaf2FullName, + id: '0.1', keyApproximate: 0.01, keyCount: 1, name: '110', + type: KeyTypes.Hash, + isLeaf: true, + nameBuffer: stringToBuffer(leaf2FullName), }], - fullName: 'car:', - id: '0.sz1ie1koqi8', + fullName: folderFullName, + id: '0', keyApproximate: 47.18, keyCount: 4718, name: 'car', }, { children: [], - fullName: 'test', - id: '0.snc1rc3zwg1o', + fullName: leafRootFullName, + id: '1', keyApproximate: 0.01, keyCount: 1, + type: KeyTypes.Stream, + isLeaf: true, name: 'test', - keys: mockLeafKeys + nameBuffer: stringToBuffer(leafRootFullName), }] jest.mock('uiSrc/services', () => ({ @@ -92,60 +112,58 @@ jest.mock('uiSrc/services', () => ({ useDisposableWebworker: () => ({ result: mockWebWorkerResult, run: jest.fn() }), })) -describe('KeyTree', () => { - it('Key tree delimiter should be in the document', () => { - render() +jest.mock('uiSrc/slices/browser/keys', () => ({ + ...jest.requireActual('uiSrc/slices/browser/keys'), + selectedKeyDataSelector: jest.fn().mockReturnValue(null), +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) - expect(screen.getByTestId('tree-view-delimiter-btn')).toBeInTheDocument() +describe('KeyTree', () => { + it('should be rendered', () => { + expect(render()).toBeTruthy() }) - it('Tree view panel should be in the document', () => { - const { container } = render() + it('"setBrowserTreeNodesOpen" to be called after click on folder', () => { + const onSelectedKeyMock = jest.fn() + const { getByTestId } = render() - expect(container.querySelector('[data-test-subj="tree-view-panel"]')).toBeInTheDocument() - }) + // set open state + fireEvent.click(getByTestId(`node-item_${folderFullName}`)) - it('Key list panel should be in the document', () => { - const { container } = render() + const expectedActions = [ + setBrowserTreeNodesOpen({ [folderFullName]: true }), + ] - expect(container.querySelector('[data-test-subj="key-list-panel"]')).toBeInTheDocument() + expect(store.getActions()).toEqual(expect.arrayContaining(expectedActions)) }) - it.skip('"setBrowserTreeNodesOpen" should be called for Open a node', async () => { - jest.useFakeTimers() - render() + it('"selectKey" to be called after click on leaf', async () => { + const onSelectedKeyMock = jest.fn() + const { getByTestId } = render() - await act(() => { - jest.advanceTimersByTime(1000) - }) + // open parent folder + fireEvent.click(getByTestId(`node-item_${folderFullName}`)) - await act(() => { - fireEvent.click(screen.getByTestId(`node-item_${mockVirtualTreeResult?.[0]?.fullName}`)) - }) + // click on the leaf + fireEvent.click(getByTestId(`node-item_${leaf2FullName}`)) - const expectedActions = [ - setBrowserTreeSelectedLeaf({}), - setBrowserTreeNodesOpen({}) - ] - - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions) - ) + expect(onSelectedKeyMock).toBeCalled() }) - it.skip('"setSearchMatch" should be called after "onChange"', () => { - const searchTerm = 'a' - - render() + it('selected key from key list should be opened and selected in the tree', async () => { + const selectedKeyDataSelectorMock = jest.fn().mockReturnValue({ + name: stringToBuffer(leaf2FullName), + nameString: leaf2FullName, + }); - fireEvent.change(screen.getByTestId('search-key'), { - target: { value: searchTerm }, - }) + (selectedKeyDataSelector as jest.Mock).mockImplementation(selectedKeyDataSelectorMock) - const expectedActions = [setSearchMatch(searchTerm)] + const { getByTestId } = render() - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions) - ) + expect(getByTestId(`node-item_${leaf2FullName}`)).toBeInTheDocument() }) }) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx index 83401aff22..76a1552393 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx @@ -1,33 +1,33 @@ -import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState, useTransition } from 'react' -import cx from 'classnames' -import { EuiResizableContainer } from '@elastic/eui' +import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { isEmpty } from 'lodash' +import cx from 'classnames' +import { useParams } from 'react-router-dom' import { appContextBrowserTree, resetBrowserTree, appContextDbConfig, setBrowserTreeNodesOpen, - setBrowserTreeSelectedLeaf } from 'uiSrc/slices/app/context' import { constructKeysToTree } from 'uiSrc/helpers' -import VirtualTree from 'uiSrc/components/virtual-tree' +import VirtualTree from 'uiSrc/pages/browser/components/virtual-tree' import TreeViewSVG from 'uiSrc/assets/img/icons/treeview.svg' import { KeysStoreData } from 'uiSrc/slices/interfaces/keys' import { Nullable, bufferToString } from 'uiSrc/utils' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' -import { KeyTypes } from 'uiSrc/constants' -import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces' +import { deleteKeyAction, selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry' import { GetKeyInfoResponse } from 'apiSrc/modules/browser/dto' -import KeyTreeDelimiter from './KeyTreeDelimiter' -import KeyList from '../key-list' +import NoKeysMessage from '../no-keys-message' import styles from './styles.module.scss' export interface Props { keysState: KeysStoreData loading: boolean + deleting: boolean commonFilterType: Nullable selectKey: ({ rowData }: { rowData: any }) => void loadMoreItems: ( @@ -54,24 +54,19 @@ const KeyTree = forwardRef((props: Props, ref) => { keysState, onDelete, commonFilterType, + deleting, onAddKeyPanel, - onBulkActionsPanel + onBulkActionsPanel, } = props - const firstPanelId = 'tree' - const secondPanelId = 'keys' - - const { panelSizes, openNodes, selectedLeaf } = useSelector(appContextBrowserTree) - const { treeViewDelimiter: delimiter = '' } = useSelector(appContextDbConfig) + const { instanceId } = useParams<{ instanceId: string }>() + const { openNodes } = useSelector(appContextBrowserTree) + const { treeViewDelimiter: delimiter = '', treeViewSort: sorting } = useSelector(appContextDbConfig) + const { nameString: selectedKeyName = null } = useSelector(selectedKeyDataSelector) ?? {} - const [,startTransition] = useTransition() - - const [statusSelected, setStatusSelected] = useState(selectedLeaf) const [statusOpen, setStatusOpen] = useState(openNodes) - const [sizes, setSizes] = useState(panelSizes) - const [keyListState, setKeyListState] = useState(keysState) const [constructingTree, setConstructingTree] = useState(false) - const [selectDefaultLeaf, setSelectDefaultLeaf] = useState(isEmpty(selectedLeaf)) + const [firstDataLoaded, setFirstDataLoaded] = useState(!!keysState.keys.length) const [items, setItems] = useState(parseKeyNames(keysState.keys ?? [])) const dispatch = useDispatch() @@ -83,19 +78,25 @@ const KeyTree = forwardRef((props: Props, ref) => { })) useEffect(() => { - updateKeysList() + openSelectedKey(selectedKeyName) }, []) useEffect(() => { setStatusOpen(openNodes) }, [openNodes]) - useEffect(() => { - setStatusSelected(selectedLeaf) - updateKeysList(Object.values(selectedLeaf)?.[0]) + // open all parents for selected key + const openSelectedKey = (selectedKeyName: Nullable = '') => { + if (selectedKeyName) { + const parts = selectedKeyName.split(delimiter) + const parents = parts.map((_, index) => parts.slice(0, index + 1).join(delimiter) + delimiter) - setSelectDefaultLeaf(isEmpty(selectedLeaf)) - }, [selectedLeaf]) + // remove key name from parents + parents.pop() + + parents.forEach((parent) => handleStatusOpen(parent, true)) + } + } useEffect(() => { setItems(parseKeyNames(keysState.keys)) @@ -106,8 +107,13 @@ const KeyTree = forwardRef((props: Props, ref) => { }, [keysState.keys]) useEffect(() => { + setFirstDataLoaded(true) setItems(parseKeyNames(keysState.keys)) - }, [delimiter, keysState.lastRefreshTime]) + }, [sorting, delimiter, keysState.lastRefreshTime]) + + useEffect(() => { + openSelectedKey(selectedKeyName) + }, [selectedKeyName]) const onLoadMoreItems = (props: { startIndex: number, stopIndex: number }) => { const formattedAllKeys = parseKeyNames(keysState.keys) @@ -117,38 +123,14 @@ const KeyTree = forwardRef((props: Props, ref) => { // select default leaf "Keys" after each change delimiter, filter or search const updateSelectedKeys = () => { dispatch(resetBrowserTree()) - - setTimeout(() => { - startTransition(() => { - setStatusSelected({}) - setSelectDefaultLeaf(true) - }) - }, 0) + openSelectedKey(selectedKeyName) } - const updateKeysList = (items:any = {}) => { - startTransition(() => { - const newState:KeysStoreData = { - ...keyListState, - keys: Object.values(items) - } - - setKeyListState(newState) - }) - } - - const onPanelWidthChange = useCallback((newSizes: any) => { - setSizes((prevSizes: any) => ({ - ...prevSizes, - ...newSizes, - })) - }, []) - - const handleStatusOpen = (name: string, value:boolean) => { + const handleStatusOpen = (name: string, value: boolean) => { setStatusOpen((prevState) => { const newState = { ...prevState } // add or remove opened node - if (newState[name]) { + if (!value) { delete newState[name] } else { newState[name] = value @@ -159,85 +141,72 @@ const KeyTree = forwardRef((props: Props, ref) => { }) } - const handleStatusSelected = (fullName: string, keys: any) => { - dispatch(setBrowserTreeSelectedLeaf({ [fullName]: keys })) + const handleStatusSelected = (name: RedisString) => { + selectKey({ rowData: { name } }) + } + + const handleDeleteLeaf = (key: RedisResponseBuffer) => { + dispatch(deleteKeyAction(key, () => { + onDelete(key) + })) + } + + const handleDeleteClicked = (type: KeyTypes | ModulesKeyTypes) => { + sendEventTelemetry({ + event: TelemetryEvent.TREE_VIEW_KEY_DELETE_CLICKED, + eventData: { + databaseId: instanceId, + keyType: type, + source: 'keyList' + } + }) + } + + if (keysState.keys.length === 0) { + const NoItemsMessage = () => { + if (loading || !firstDataLoaded) { + return loading... + } + + return ( + + ) + } + + return ( +
+
+ +
+
+ ) } return ( -
+
-
- - {(EuiResizablePanel, EuiResizableButton) => ( - <> - -
-
- -
-
- setSelectDefaultLeaf(false)} - /> -
-
-
- - - - -
- -
-
- - )} -
-
+
) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx deleted file mode 100644 index 5df4b8cebd..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { cloneDeep } from 'lodash' -import React from 'react' -import { instance, mock } from 'ts-mockito' -import { DEFAULT_DELIMITER } from 'uiSrc/constants' -import { resetBrowserTree, setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' -import { - cleanup, - clearStoreActions, - fireEvent, - mockedStore, - render, - screen, - act, -} from 'uiSrc/utils/test-utils' - -import KeyTreeDelimiter, { Props } from './KeyTreeDelimiter' - -const mockedProps = mock() -let store: typeof mockedStore -const INLINE_ITEM_EDITOR = 'inline-item-editor' -const INLINE_EDITOR_APPLY_BTN = 'apply-btn' -const DELIMITER_TRIGGER_BTN = 'tree-view-delimiter-btn' -const DELIMITER_INPUT = 'tree-view-delimiter-input' - -beforeEach(() => { - cleanup() - store = cloneDeep(mockedStore) - store.clearActions() -}) - -jest.mock('uiSrc/services', () => ({ - localStorageService: { - set: jest.fn(), - get: jest.fn(), - }, -})) - -describe('KeyTreeDelimiter', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) - - it('Delimiter button should be rendered', () => { - render() - - expect(screen.getByTestId(DELIMITER_TRIGGER_BTN)).toBeInTheDocument() - }) - - it('Delimiter input should be rendered after click on button', async () => { - render() - - await act(() => { - fireEvent.click(screen.getByTestId(DELIMITER_TRIGGER_BTN)) - }) - - expect(screen.getByTestId(DELIMITER_INPUT)).toBeInTheDocument() - }) - - it('"setBrowserTreeDelimiter" should be called after Apply change delimiter', async () => { - const value = 'val' - render() - - await act(() => { - fireEvent.click(screen.getByTestId(DELIMITER_TRIGGER_BTN)) - }) - - fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value } }) - - await act(() => { - fireEvent.click(screen.getByTestId(INLINE_EDITOR_APPLY_BTN)) - }) - - const expectedActions = [ - setBrowserTreeDelimiter(value), - resetBrowserTree(), - ] - - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions) - ) - }) - - it('"setBrowserTreeDelimiter" should be called with DEFAULT_DELIMITER after Apply change with empty input', async () => { - const value = '' - render() - - await act(() => { - fireEvent.click(screen.getByTestId(DELIMITER_TRIGGER_BTN)) - }) - - fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value } }) - - await act(() => { - fireEvent.click(screen.getByTestId(INLINE_EDITOR_APPLY_BTN)) - }) - - const expectedActions = [ - setBrowserTreeDelimiter(DEFAULT_DELIMITER), - resetBrowserTree(), - ] - - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions) - ) - }) -}) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx deleted file mode 100644 index 844065037a..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState } from 'react' -import cx from 'classnames' -import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' -import { EuiIcon, EuiPopover } from '@elastic/eui' - -import { replaceSpaces } from 'uiSrc/utils' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import InlineItemEditor from 'uiSrc/components/inline-item-editor' -import { DEFAULT_DELIMITER } from 'uiSrc/constants' -import { appContextDbConfig, resetBrowserTree, setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' - -import styles from './styles.module.scss' - -export interface Props { - loading: boolean -} -const MAX_DELIMITER_LENGTH = 5 -const KeyTreeDelimiter = ({ loading }: Props) => { - const { instanceId = '' } = useParams<{ instanceId: string }>() - const { treeViewDelimiter: delimiter = '' } = useSelector(appContextDbConfig) - const [isPopoverOpen, setIsPopoverOpen] = useState(false) - - const dispatch = useDispatch() - - const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen) - const closePopover = () => setIsPopoverOpen(false) - - const button = ( -
{}} - data-testid="tree-view-delimiter-btn" - > - {replaceSpaces(delimiter)} - -
- ) - - const handleApplyDelimiter = (value: string) => { - sendEventTelemetry({ - event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED, - eventData: { - databaseId: instanceId, - from: delimiter, - to: value || DEFAULT_DELIMITER - } - }) - closePopover() - dispatch(setBrowserTreeDelimiter(value || DEFAULT_DELIMITER)) - - dispatch(resetBrowserTree()) - } - - return ( -
- -
Delimiter
-
- closePopover()} - onApply={(value) => handleApplyDelimiter(value)} - /> -
-
-
- ) -} - -export default React.memo(KeyTreeDelimiter) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/index.ts b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/index.ts deleted file mode 100644 index 88dc65dd7b..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import KeyTreeDelimiter from './KeyTreeDelimiter' - -export default KeyTreeDelimiter diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/styles.module.scss deleted file mode 100644 index 3320d27cc7..0000000000 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/styles.module.scss +++ /dev/null @@ -1,68 +0,0 @@ -$selectDelimiterHeight: 18px; - -.anchorBtn { - width: 60px; - height: $selectDelimiterHeight; - margin-left: 6px; - padding-left: 6px; - margin-bottom: 10px; - border: 1px solid var(--separatorColor) !important; - border-radius: 4px; - font-size: 12px; - line-height: 16px; - - &Open { - border-bottom: 2px solid var(--euiColorPrimary) !important; - } - - svg { - position: absolute; - width: 12px !important; - height: 12px !important; - right: 5px; - top: 3px; - } -} - -.popoverWrapper { - height: 84px; - width: 182px; - padding: 12px 18px !important; - border: 1px solid var(--euiColorPrimary) !important; - background-color: var(--euiColorLightestShade) !important; - margin-top: -18px; - - :global(.euiPopover__panelArrow) { - &::before, - &::after { - content: none !important; - } - } - - .input { - display: inline-block; - width: 72px; - - input { - height: 36px !important; - border-radius: 4px; - background-color: var(--browserViewTypePassive) !important; - } - } -} - -.inputLabel { - display: inline-block; - width: 70px; - font-size: 14px; - color: var(--euiTextSubduedColor)!important; -} - -.input { - display: inline-block; - width: 72px; - - input { - background-color: var(--euiColorEmptyShade) !important; - } -} diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.spec.tsx new file mode 100644 index 0000000000..591860f190 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.spec.tsx @@ -0,0 +1,154 @@ +import { cloneDeep } from 'lodash' +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { DEFAULT_DELIMITER, SortOrder } from 'uiSrc/constants' +import { resetBrowserTree, setBrowserTreeDelimiter, setBrowserTreeSort } from 'uiSrc/slices/app/context' +import { + cleanup, + clearStoreActions, + fireEvent, + mockedStore, + render, + screen, + act, + waitForEuiPopoverVisible, +} from 'uiSrc/utils/test-utils' + +import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry' +import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers' +import KeyTreeSettings, { Props } from './KeyTreeSettings' + +const mockedProps = mock() +let store: typeof mockedStore +const APPLY_BTN = 'tree-view-apply-btn' +const TREE_SETTINGS_TRIGGER_BTN = 'tree-view-settings-btn' +const SORTING_SELECT = 'tree-view-sorting-select' +const DELIMITER_INPUT = 'tree-view-delimiter-input' +const SORTING_DESC_ITEM = 'tree-view-sorting-item-DESC' + +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +jest.mock('uiSrc/services', () => ({ + localStorageService: { + set: jest.fn(), + get: jest.fn(), + }, +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +describe('KeyTreeDelimiter', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('Settings button should be rendered', () => { + render() + + expect(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN)).toBeInTheDocument() + }) + + it('Delimiter input and Sorting selector should be rendered after click on button', async () => { + render() + + await act(async () => { + fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN)) + }) + await waitForEuiPopoverVisible() + + expect(screen.getByTestId(DELIMITER_INPUT)).toBeInTheDocument() + expect(screen.getByTestId(SORTING_SELECT)).toBeInTheDocument() + }) + + it('"setBrowserTreeDelimiter" and "setBrowserTreeSort" should be called after Apply change delimiter', async () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + const value = 'val' + render() + + await act(() => { + fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN)) + }) + + await waitForEuiPopoverVisible() + + fireEvent.change(screen.getByTestId(DELIMITER_INPUT), { target: { value } }) + + await act(() => { + fireEvent.click(screen.getByTestId(SORTING_SELECT)) + }) + + await waitForEuiPopoverVisible() + + await act(() => { + fireEvent.click(screen.getByTestId(SORTING_DESC_ITEM)) + }) + + await act(() => { + fireEvent.click(screen.getByTestId(APPLY_BTN)) + }) + + const expectedActions = [ + setBrowserTreeDelimiter(value), + resetBrowserTree(), + setBrowserTreeSort(SortOrder.DESC), + resetBrowserTree(), + ] + + expect(clearStoreActions(store.getActions())).toEqual( + clearStoreActions(expectedActions) + ) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + from: DEFAULT_DELIMITER, + to: value, + } + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.TREE_VIEW_KEYS_SORTED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + sorting: SortOrder.DESC, + } + }); + + (sendEventTelemetry as jest.Mock).mockRestore() + }) + + it('"setBrowserTreeDelimiter" should be called with DEFAULT_DELIMITER after Apply change with empty input', async () => { + const value = '' + render() + + await act(() => { + fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN)) + }) + + await waitForEuiPopoverVisible() + + fireEvent.change(screen.getByTestId(DELIMITER_INPUT), { target: { value } }) + + await act(() => { + fireEvent.click(screen.getByTestId(APPLY_BTN)) + }) + + const expectedActions = [ + setBrowserTreeDelimiter(DEFAULT_DELIMITER), + resetBrowserTree(), + ] + + expect(clearStoreActions(store.getActions())).toEqual( + clearStoreActions(expectedActions) + ) + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx new file mode 100644 index 0000000000..acc3011a46 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx @@ -0,0 +1,176 @@ +import React, { useCallback, useEffect, useState } from 'react' +import cx from 'classnames' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { EuiButton, EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPopover, EuiSuperSelect, EuiText } from '@elastic/eui' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { DEFAULT_DELIMITER, DEFAULT_TREE_SORTING, SortOrder } from 'uiSrc/constants' +import { + appContextDbConfig, + resetBrowserTree, + setBrowserTreeDelimiter, + setBrowserTreeSort, +} from 'uiSrc/slices/app/context' +import { ReactComponent as TreeViewSort } from 'uiSrc/assets/img/browser/treeViewSort.svg' + +import styles from './styles.module.scss' + +export interface Props { + loading: boolean +} +const MAX_DELIMITER_LENGTH = 5 +const sortOptions = [SortOrder.ASC, SortOrder.DESC].map((value) => ({ + value, + inputDisplay: ( + Key name {value} + ), +})) + +const KeyTreeSettings = ({ loading }: Props) => { + const { instanceId = '' } = useParams<{ instanceId: string }>() + const { treeViewDelimiter = '', treeViewSort = DEFAULT_TREE_SORTING } = useSelector(appContextDbConfig) + const [sorting, setSorting] = useState(treeViewSort) + const [delimiter, setDelimiter] = useState(treeViewDelimiter) + + const [isPopoverOpen, setIsPopoverOpen] = useState(false) + + const dispatch = useDispatch() + + useEffect(() => { + setSorting(treeViewSort) + }, [treeViewSort]) + + useEffect(() => { + setDelimiter(treeViewDelimiter) + }, [treeViewDelimiter]) + + const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen) + const closePopover = () => { + setIsPopoverOpen(false) + setTimeout(() => { + resetStates() + }, 500) + } + + const resetStates = useCallback(() => { + setSorting(treeViewSort) + setDelimiter(treeViewDelimiter) + }, [treeViewSort, treeViewDelimiter]) + + const button = ( + + ) + + const handleApply = () => { + if (delimiter !== treeViewDelimiter) { + dispatch(setBrowserTreeDelimiter(delimiter || DEFAULT_DELIMITER)) + sendEventTelemetry({ + event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED, + eventData: { + databaseId: instanceId, + from: treeViewDelimiter, + to: delimiter || DEFAULT_DELIMITER + } + }) + + dispatch(resetBrowserTree()) + } + + if (sorting !== treeViewSort) { + dispatch(setBrowserTreeSort(sorting)) + + sendEventTelemetry({ + event: TelemetryEvent.TREE_VIEW_KEYS_SORTED, + eventData: { + databaseId: instanceId, + sorting: sorting || DEFAULT_TREE_SORTING, + } + }) + + dispatch(resetBrowserTree()) + } + + setIsPopoverOpen(false) + } + + const onChangeSort = (value: SortOrder) => { + setSorting(value) + } + + return ( +
+ + + + Filters + + +
Delimiter
+ setDelimiter(e.target.value)} + aria-label="Title" + maxLength={MAX_DELIMITER_LENGTH} + data-testid="tree-view-delimiter-input" + /> +
+ +
+ + Sort by +
+ onChangeSort(value)} + data-testid="tree-view-sorting-select" + /> +
+ +
+ + Cancel + + + Apply + +
+
+
+
+
+ ) +} + +export default React.memo(KeyTreeSettings) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/index.ts b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/index.ts new file mode 100644 index 0000000000..f748fe085d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/index.ts @@ -0,0 +1,3 @@ +import KeyTreeSettings from './KeyTreeSettings' + +export default KeyTreeSettings diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss new file mode 100644 index 0000000000..9118c31517 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss @@ -0,0 +1,84 @@ +.container { + margin-left: 8px; + margin-top: 1px; + + :global(.euiPopover), .anchorWrapper { + height: 100%; + } +} + +.anchorBtn { + width: 24px; + height: 100% !important; + + svg { + width: 18px !important; + height: 18px !important; + } +} + +.popoverWrapper { + height: 162px; + width: 300px; + padding: 14px 16px !important; + border: none !important; + background-color: var(--euiColorLightestShade) !important; + + :global { + .euiPopover__panelArrow { + &::before, + &::after { + border-bottom-color: var(--euiColorLightestShade) !important; + } + } + .euiFormControlLayout { + height: 26px !important; + width: auto; + } + } + + .input, .select { + width: 188px; + height: 24px !important; + font-size: 12px; + border-radius: 4px; + background-color: var(--browserViewTypePassive) !important; + } +} + +.label { + display: flex; + width: 66px; + font-size: 12px; + color: var(--euiTextSubduedColor)!important; +} + +.title { + font-size: 14px; + font-weight: 500; +} + +.row { + flex-direction: row !important; + align-items: center; + justify-content: space-between; +} + +.sortIcon { + margin-right: 4px; +} + +.selectItem { + min-height: 24px !important; +} + +.footer { + width: 100%; + display: flex; + justify-content: flex-end; + padding-top: 4px; + + button { + margin-left: 8px; + } +} diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/index.ts b/redisinsight/ui/src/pages/browser/components/key-tree/index.ts index 24664563b0..dec23b355f 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/index.ts +++ b/redisinsight/ui/src/pages/browser/components/key-tree/index.ts @@ -1,3 +1,8 @@ import KeyTree from './KeyTree' +import KeyTreeSettings from './KeyTreeSettings' export default KeyTree + +export { + KeyTreeSettings, +} diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-tree/styles.module.scss index 89dc51c251..c07d7ec3e0 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/key-tree/styles.module.scss @@ -2,7 +2,7 @@ @import '@elastic/eui/src/components/table/mixins'; @import '@elastic/eui/src/global_styling/index'; -.page { +.container { height: 100%; overflow: hidden; } @@ -11,67 +11,27 @@ max-width: 372px !important; } +.noKeys { + @include euiScrollBar; + overflow: auto; + + text-align: center; + margin: auto; +} + .content { width: 100%; + display: flex; + flex-direction: column; height: 100%; background-color: var(--euiColorEmptyShade); border-top: 1px solid var(--euiColorLightShade); -} - -.body { - display: flex; - height: 100%; :global(.ReactVirtualized__Table__headerRow) { border: none !important; } } -.resizablePanelLeft { - border-right: 1px solid var(--euiColorLightShade); -} -.resizablePanelLeft, -.resizablePanelRight { - :global(.euiResizablePanel__content) { - padding-right: 0px !important; - } -} - -.resizableButton { - z-index: 1 !important; -} -.resizableButton:not(:hover) { - &::before, &::after { - width: 0 !important; - } -} - -.tree { - @include euiScrollBar; - - border-bottom: none; - position: relative; - padding-top: 6px; - - height: 100%; - - flex: 1; - display: flex; - flex-direction: column; -} - -.treeContent { - height: 100%; - position: relative; - width: 100%; -} - -.list { - height: 100%; - flex: 3; - min-width: 400px; -} - .filter { display: inline-block; width: 100px; diff --git a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx index 24102f588f..52739d3f95 100644 --- a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx +++ b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx @@ -20,9 +20,8 @@ import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiS import { OnboardingStepName, OnboardingSteps } from 'uiSrc/constants/onboarding' import { incrementOnboardStepAction } from 'uiSrc/slices/app/features' -import { OnboardingTour } from 'uiSrc/components' +import { AutoRefresh, OnboardingTour } from 'uiSrc/components' import { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features' -import AutoRefresh from '../auto-refresh' import styles from './styles.module.scss' diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss b/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss index 7d8902e5ea..d747185b18 100644 --- a/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/no-keys-found/styles.module.scss @@ -1,7 +1,8 @@ .container { max-width: 400px; + min-height: 440px; - margin: -10vh auto 0; + margin: auto; text-align: center; } diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx new file mode 100644 index 0000000000..de130e1c72 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx @@ -0,0 +1,176 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { cloneDeep } from 'lodash' +import { render, mockedStore, cleanup } from 'uiSrc/utils/test-utils' + +import { keysSelector } from 'uiSrc/slices/browser/keys' +import { SearchMode } from 'uiSrc/slices/interfaces/keys' +import { redisearchSelector } from 'uiSrc/slices/browser/redisearch' +import NoKeysMessage, { Props } from './NoKeysMessage' + +const mockedProps = mock() + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +jest.mock('uiSrc/slices/browser/keys', () => ({ + ...jest.requireActual('uiSrc/slices/browser/keys'), + keysSelector: jest.fn().mockReturnValue({ + searchMode: 'Pattern', + filter: null, + search: '', + viewType: 'Browser', + }), +})) + +jest.mock('uiSrc/slices/browser/redisearch', () => ({ + ...jest.requireActual('uiSrc/slices/browser/redisearch'), + redisearchSelector: jest.fn().mockReturnValue({ + search: '', + isSearched: false, + selectedIndex: null, + }), +})) + +describe('NoKeysMessage', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + describe('SearchMode = Pattern', () => { + it('NoKeysFound should be rendered if total=0', () => { + const { queryByTestId } = render() + expect(queryByTestId('no-result-found-msg')).toBeInTheDocument() + }) + + it('"scan-no-results-found" should be rendered if searched and scanned < total', () => { + const keysSelectorMock = jest.fn().mockReturnValue({ + isSearched: true, + searchMode: SearchMode.Pattern, + }) + const total = 100; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock) + const { queryByTestId } = render( + + ) + + expect(queryByTestId('scan-no-results-found')).toBeInTheDocument() + }) + + it('"no-result-found" should be rendered if searched and scanned===total', () => { + const keysSelectorMock = jest.fn().mockReturnValue({ + isSearched: true, + searchMode: SearchMode.Pattern, + }) + const total = 100; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock) + const { container } = render( + + ) + + expect(container.querySelector('[data-test-subj="no-result-found"]')).toBeInTheDocument() + }) + + it('"scan-no-results-found" should be rendered if filtered and scanned { + const keysSelectorMock = jest.fn().mockReturnValue({ + isFiltered: true, + searchMode: SearchMode.Pattern, + }) + const total = 100; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock) + const { queryByTestId } = render( + + ) + + expect(queryByTestId('scan-no-results-found')).toBeInTheDocument() + }) + }) + + describe('SearchMode = RediSearch', () => { + it('"no-result-select-index" should be rendered if searched and scanned < total', () => { + const keysSelectorMock = jest.fn().mockReturnValue({ + isSearched: true, + searchMode: SearchMode.Redisearch, + }) + const redisearchSelectorMock = jest.fn().mockReturnValue({ + selectedIndex: null + }) + const total = 100; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock); + (redisearchSelector as jest.Mock).mockImplementation(redisearchSelectorMock) + const { queryByTestId } = render( + + ) + + expect(queryByTestId('no-result-select-index')).toBeInTheDocument() + }) + it('"no-result-found-only" should be rendered total = 0', () => { + const keysSelectorMock = jest.fn().mockReturnValue({ + isSearched: true, + searchMode: SearchMode.Redisearch, + }) + const redisearchSelectorMock = jest.fn().mockReturnValue({ + selectedIndex: '123' + }) + const total = 0; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock); + (redisearchSelector as jest.Mock).mockImplementation(redisearchSelectorMock) + const { queryByTestId } = render( + + ) + + expect(queryByTestId('no-result-found-only')).toBeInTheDocument() + }) + + it('"no-result-found-only" should be rendered if searched and scanned { + const keysSelectorMock = jest.fn().mockReturnValue({ + searchMode: SearchMode.Redisearch, + }) + const redisearchSelectorMock = jest.fn().mockReturnValue({ + isSearched: true, + selectedIndex: '123', + }) + const total = 100; + + (keysSelector as jest.Mock).mockImplementation(keysSelectorMock); + (redisearchSelector as jest.Mock).mockImplementation(redisearchSelectorMock) + const { queryByTestId } = render( + + ) + + expect(queryByTestId('no-result-found-only')).toBeInTheDocument() + }) + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx new file mode 100644 index 0000000000..a0f1762608 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx @@ -0,0 +1,64 @@ +import React from 'react' + +import { useSelector } from 'react-redux' +import { SearchMode } from 'uiSrc/slices/interfaces/keys' + +import { + FullScanNoResultsFoundText, + NoResultsFoundText, + NoSelectedIndexText, + ScanNoResultsFoundText, +} from 'uiSrc/constants/texts' +import { keysSelector } from 'uiSrc/slices/browser/keys' +import { redisearchSelector } from 'uiSrc/slices/browser/redisearch' + +import NoKeysFound from '../no-keys-found' + +export interface Props { + total: number + scanned: number + onAddKeyPanel: (value: boolean) => void + onBulkActionsPanel: (value: boolean) => void +} + +const NoKeysMessage = (props: Props) => { + const { + total, + scanned, + onAddKeyPanel, + onBulkActionsPanel, + } = props + + const { selectedIndex, isSearched: redisearchIsSearched } = useSelector(redisearchSelector) + const { isSearched: patternIsSearched, isFiltered, searchMode } = useSelector(keysSelector) + + if (searchMode === SearchMode.Redisearch) { + if (!selectedIndex) { + return NoSelectedIndexText + } + + if (total === 0) { + return NoResultsFoundText + } + + if (redisearchIsSearched) { + return scanned < total ? NoResultsFoundText : FullScanNoResultsFoundText + } + } + + if (total === 0) { + return () + } + + if (patternIsSearched) { + return scanned < total ? ScanNoResultsFoundText : FullScanNoResultsFoundText + } + + if (isFiltered && scanned < total) { + return ScanNoResultsFoundText + } + + return NoResultsFoundText +} + +export default NoKeysMessage diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-message/index.ts b/redisinsight/ui/src/pages/browser/components/no-keys-message/index.ts new file mode 100644 index 0000000000..c3107e6843 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/no-keys-message/index.ts @@ -0,0 +1,3 @@ +import NoKeysMessage from './NoKeysMessage' + +export default NoKeysMessage diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/RejsonDetailsWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/components/rejson-details/RejsonDetailsWrapper.spec.tsx deleted file mode 100644 index 3053b97b69..0000000000 --- a/redisinsight/ui/src/pages/browser/components/rejson-details/RejsonDetailsWrapper.spec.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' -import { render } from 'uiSrc/utils/test-utils' -import RejsonDetailsWrapper from './RejsonDetailsWrapper' - -describe('ReJSONDetailsWrapper', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) -}) diff --git a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx index 8787ada41f..fbd3ff302d 100644 --- a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx @@ -22,6 +22,7 @@ import { } from 'uiSrc/slices/browser/redisearch' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { resetBrowserTree } from 'uiSrc/slices/app/context' import styles from './styles.module.scss' const placeholders = { @@ -66,6 +67,10 @@ const SearchKeyList = () => { dispatch(setSearchMatch(match, searchMode)) + if (viewType === KeyViewType.Tree) { + dispatch(resetBrowserTree()) + } + dispatch(fetchKeys( { searchMode, diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/index.ts b/redisinsight/ui/src/pages/browser/components/stream-details/index.ts deleted file mode 100644 index b1726a2381..0000000000 --- a/redisinsight/ui/src/pages/browser/components/stream-details/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import StreamDetailsWrapper from './StreamDetailsWrapper' - -export default StreamDetailsWrapper diff --git a/redisinsight/ui/src/components/virtual-tree/VirtualTree.spec.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.spec.tsx similarity index 58% rename from redisinsight/ui/src/components/virtual-tree/VirtualTree.spec.tsx rename to redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.spec.tsx index fcd74e9a79..77ab0073a2 100644 --- a/redisinsight/ui/src/components/virtual-tree/VirtualTree.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.spec.tsx @@ -15,38 +15,36 @@ const mockedItems = [ }, ] -export const mockLeafKeys = { - test: { name: 'test', type: 'hash', ttl: -1, size: 9849176 } -} - -export const mockVirtualTreeResult = [{ - children: [{ +export const mockVirtualTreeResult = [ + { + children: [{ + children: [], + fullName: 'car:110:', + id: '0.snc1rc3zwgo', + keyApproximate: 0.01, + keyCount: 1, + name: '110', + }], + fullName: 'car:', + id: '0.sz1ie1koqi8', + keyApproximate: 47.18, + keyCount: 4718, + name: 'car', + }, + { children: [], - fullName: 'car:110:', - id: '0.snc1rc3zwgo', + fullName: 'test', + id: '0.snc1rc3zwg1o', keyApproximate: 0.01, keyCount: 1, - name: '110', - }], - fullName: 'car:', - id: '0.sz1ie1koqi8', - keyApproximate: 47.18, - keyCount: 4718, - name: 'car', -}, -{ - children: [], - fullName: 'test', - id: '0.snc1rc3zwg1o', - keyApproximate: 0.01, - keyCount: 1, - name: 'test', - keys: mockLeafKeys -}] + name: 'test', + } +] jest.mock('uiSrc/services', () => ({ + __esModule: true, ...jest.requireActual('uiSrc/services'), - useDisposableWebworker: () => ({ result: mockVirtualTreeResult, run: jest.fn() }), + useDisposableWebworker: () => ({ result: mockVirtualTreeResult, run: jest.fn() }) })) describe('VirtualTree', () => { @@ -80,23 +78,18 @@ describe('VirtualTree', () => { expect(queryByTestId('node-item_test')).toBeInTheDocument() }) - it('should select first leaf "Keys" by default', async () => { - const mockConstructingTreeFn = jest.fn() - const mockOnStatusSelected = jest.fn() - const mockOnSelectLeaf = jest.fn() + it('should not call onStatusOpen if more than one folder is exist', () => { + const mockFn = jest.fn() + const mockOnStatusOpen = jest.fn() render( ) - expect(mockOnSelectLeaf).toHaveBeenCalledWith(mockLeafKeys) - expect(mockOnStatusSelected).toHaveBeenCalledWith('test', mockLeafKeys) + expect(mockOnStatusOpen).not.toHaveBeenCalled() }) }) diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx new file mode 100644 index 0000000000..5092080298 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx @@ -0,0 +1,288 @@ +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react' +import AutoSizer from 'react-virtualized-auto-sizer' +import { debounce, get, set } from 'lodash' +import { + TreeWalker, + TreeWalkerValue, + FixedSizeTree as Tree, +} from 'react-vtree' +import { EuiIcon, EuiLoadingSpinner, EuiProgress } from '@elastic/eui' +import { useDispatch } from 'react-redux' + +import { bufferToString, Maybe, Nullable } from 'uiSrc/utils' +import { useDisposableWebworker } from 'uiSrc/services' +import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' +import { ThemeContext } from 'uiSrc/contexts/themeContext' +import { DEFAULT_DELIMITER, DEFAULT_TREE_SORTING, KeyTypes, ModulesKeyTypes, SortOrder, Theme } from 'uiSrc/constants' +import KeyLightSVG from 'uiSrc/assets/img/sidebar/browser.svg' +import KeyDarkSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' +import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces' +import { fetchKeysMetadataTree } from 'uiSrc/slices/browser/keys' +import { GetKeyInfoResponse } from 'apiSrc/modules/browser/dto' + +import { Node } from './components/Node' +import { NodeMeta, TreeData, TreeNode } from './interfaces' + +import styles from './styles.module.scss' + +export interface Props { + items: IKeyPropTypes[] + delimiter?: string + loadingIcon?: string + loading: boolean + deleting: boolean + sorting: Maybe + commonFilterType: Nullable + statusSelected: Nullable, + statusOpen: OpenedNodes + webworkerFn: (...args: any) => any + onStatusOpen?: (name: string, value: boolean) => void + onStatusSelected?: (key: RedisString) => void + setConstructingTree: (status: boolean) => void + onDeleteLeaf: (key: RedisResponseBuffer) => void + onDeleteClicked: (type: KeyTypes | ModulesKeyTypes) => void +} + +interface OpenedNodes { + [key: string]: boolean +} + +export const KEYS = 'keys' + +const VirtualTree = (props: Props) => { + const { + items, + delimiter = DEFAULT_DELIMITER, + loadingIcon = 'empty', + statusOpen = {}, + statusSelected, + loading, + deleting, + sorting = DEFAULT_TREE_SORTING, + commonFilterType, + onStatusOpen, + onStatusSelected, + setConstructingTree, + webworkerFn = () => { }, + onDeleteClicked, + onDeleteLeaf, + } = props + + const { theme } = useContext(ThemeContext) + const [rerenderState, rerender] = useState({}) + const controller = useRef>(null) + const elements = useRef({}) + const nodes = useRef([]) + + const { result, run: runWebworker } = useDisposableWebworker(webworkerFn) + + const dispatch = useDispatch() + + useEffect(() => + () => { + nodes.current = [] + elements.current = {} + }, []) + + // receive result from the "runWebworker" + useEffect(() => { + if (!result) { + return + } + + elements.current = {} + nodes.current = result + rerender({}) + setConstructingTree?.(false) + + openSingleFolderNode(nodes.current) + }, [result]) + + useEffect(() => { + if (!items?.length) { + nodes.current = [] + elements.current = {} + rerender({}) + runWebworker?.({ items: [], delimiter, sorting }) + return + } + + setConstructingTree(true) + runWebworker?.({ items, delimiter, sorting }) + }, [items, delimiter]) + + const handleUpdateSelected = useCallback((name: RedisString) => { + onStatusSelected?.(name) + }, [onStatusSelected]) + + const handleUpdateOpen = useCallback((fullName: string, value: boolean) => { + onStatusOpen?.(fullName, value) + }, [onStatusOpen, nodes]) + + const updateNodeByPath = (path: string, data: any) => { + const paths = path.replaceAll('.', '.children.') + + const node = get(nodes.current, paths) + const fullData = { ...node, ...data } + + if (node) { + set(nodes.current, paths, fullData) + } + } + + const formatItem = useCallback((item: GetKeyInfoResponse) => ({ + ...item, + nameString: bufferToString(item.name as string) + }), []) + + const getMetadata = useCallback(( + itemsInit: any[] = [] + ): void => { + dispatch(fetchKeysMetadataTree( + itemsInit, + commonFilterType, + controller.current?.signal, + (loadedItems) => + onSuccessFetchedMetadata(loadedItems), + () => { rerender({}) } + )) + }, [commonFilterType]) + + const onSuccessFetchedMetadata = ( + loadedItems: any[], + ) => { + const items = loadedItems.map(formatItem) + + items.forEach((item) => updateNodeByPath(item.path, item)) + + rerender({}) + } + + const getMetadataDebounced = debounce(() => { + const entries = Object.entries(elements.current) + + getMetadata(entries) + + elements.current = {} + }, 100) + + const getMetadataNode = useCallback((nameBuffer: any, path: string) => { + elements.current[path] = nameBuffer + getMetadataDebounced() + }, []) + + // This helper function constructs the object that will be sent back at the step + // [2] during the treeWalker function work. Except for the mandatory `data` + // field you can put any additional data here. + const getNodeData = ( + node: TreeNode, + nestingLevel: number, + ): TreeWalkerValue => ({ + data: { + id: node.id.toString(), + isLeaf: node.isLeaf, + keyCount: node.keyCount, + name: node.name, + nameString: node.nameString, + nameBuffer: node.nameBuffer, + ttl: node.ttl, + size: node.size, + type: node.type, + fullName: node.fullName, + shortName: node.nameString?.split(delimiter).pop(), + nestingLevel, + deleting, + path: node.path, + getMetadata: getMetadataNode, + onDeleteClicked, + updateStatusSelected: handleUpdateSelected, + updateStatusOpen: handleUpdateOpen, + onDelete: onDeleteLeaf, + leafIcon: theme === Theme.Dark ? KeyDarkSVG : KeyLightSVG, + keyApproximate: node.keyApproximate, + isSelected: !!node.isLeaf && statusSelected === node?.nameString, + isOpenByDefault: statusOpen[node.fullName], + }, + nestingLevel, + node, + }) + + const openSingleFolderNode = useCallback((treeNodes?: TreeNode[]) => { + let nodes = treeNodes + while (nodes?.length === 1) { + const singleNode = nodes[0] + onStatusOpen?.(singleNode.fullName, true) + nodes = singleNode.children + } + }, [onStatusOpen]) + + // The `treeWalker` function runs only on tree re-build which is performed + // whenever the `treeWalker` prop is changed. + const treeWalker = useCallback( + function* treeWalker(): ReturnType> { + // Step [1]: Define the root multiple nodes of our tree + for (let i = 0; i < nodes.current.length; i++) { + yield getNodeData(nodes.current[i], 0) + } + + // Step [2]: Get the parent component back. It will be the object + // the `getNodeData` function constructed, so you can read any data from it. + while (true) { + const parentMeta = yield + + for (let i = 0; i < parentMeta.node.children?.length; i++) { + // Step [3]: Yielding all the children of the provided component. Then we + // will return for the step [2] with the first children. + yield getNodeData( + parentMeta.node.children[i], + parentMeta.nestingLevel + 1, + ) + } + } + }, + [statusSelected, statusOpen, rerenderState], + ) + + return ( + + {({ height, width }) => ( +
+ { nodes.current.length > 0 && ( + <> + {loading && ( + + )} + + {Node} + + + )} + { nodes.current.length === 0 && loading && ( +
+
+ + +
+
+ )} +
+ )} +
+ ) +} + +export default VirtualTree diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.spec.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx similarity index 57% rename from redisinsight/ui/src/components/virtual-tree/components/Node/Node.spec.tsx rename to redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx index 2b39ac5f23..a5df9953fe 100644 --- a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx @@ -2,19 +2,31 @@ import React from 'react' import { NodePublicState } from 'react-vtree/dist/es/Tree' import { instance, mock } from 'ts-mockito' import { render, screen } from 'uiSrc/utils/test-utils' -import KeyDarkSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' +import { stringToBuffer } from 'uiSrc/utils' +import { KeyTypes } from 'uiSrc/constants' import Node from './Node' import { TreeData } from '../../interfaces' -import { mockLeafKeys, mockVirtualTreeResult } from '../../VirtualTree.spec' +import { mockVirtualTreeResult } from '../../VirtualTree.spec' +const mockDataFullName = 'test' const mockedProps = mock>() const mockedPropsData = mock() const mockedData: TreeData = { ...instance(mockedPropsData), nestingLevel: 3, - leafIcon: KeyDarkSVG + isLeaf: true, + path: '0.0.5.6', + fullName: mockDataFullName, + nameString: mockDataFullName, + nameBuffer: stringToBuffer(mockDataFullName), +} + +const mockedDataWithMetadata = { + ...mockedData, + type: KeyTypes.Hash, + ttl: 123, + size: 123, } -const mockDataFullName = 'test' jest.mock('uiSrc/services', () => ({ ...jest.requireActual('uiSrc/services'), @@ -39,32 +51,45 @@ describe('Node', () => { expect(container.querySelector(`[data-test-subj="node-folder-icon_${mockDataFullName}"`)).toBeInTheDocument() }) - it('should render leaf icon for Leaf properly', () => { + it('"setItems", "updateStatusSelected", "mockGetMetadata" should be called after click on Leaf', () => { + const mockUpdateStatusSelected = jest.fn() + const mockUpdateStatusOpen = jest.fn() + const mockSetOpen = jest.fn() + const mockGetMetadata = jest.fn() + const mockData: TreeData = { ...mockedData, - isLeaf: true, - fullName: mockDataFullName + updateStatusSelected: mockUpdateStatusSelected, + updateStatusOpen: mockUpdateStatusOpen, + getMetadata: mockGetMetadata, } - const { container } = render() + render() - expect(container.querySelector(`[data-test-subj="leaf-icon_${mockDataFullName}"`)).toBeInTheDocument() + screen.getByTestId(`node-item_${mockDataFullName}`).click() + + expect(mockUpdateStatusSelected).toBeCalledWith(mockData.nameBuffer) + expect(mockUpdateStatusOpen).toBeCalledWith(mockDataFullName, true) + expect(mockGetMetadata).toBeCalledWith(mockData.nameBuffer, mockData.path) + expect(mockSetOpen).not.toBeCalled() }) - it('"setItems", "updateStatusSelected" should be called after click on Leaf', () => { - const mockSetItems = jest.fn() + it('"mockGetMetadata" not be call if size and ttl exists', () => { const mockUpdateStatusSelected = jest.fn() const mockUpdateStatusOpen = jest.fn() const mockSetOpen = jest.fn() + const mockGetMetadata = jest.fn() const mockData: TreeData = { - ...mockedData, - isLeaf: true, - fullName: mockDataFullName, - keys: mockLeafKeys, - setItems: mockSetItems, + ...mockedDataWithMetadata, updateStatusSelected: mockUpdateStatusSelected, updateStatusOpen: mockUpdateStatusOpen, + getMetadata: mockGetMetadata, } render( { screen.getByTestId(`node-item_${mockDataFullName}`).click() - expect(mockSetItems).toBeCalledWith(mockLeafKeys) - expect(mockUpdateStatusSelected).toBeCalledWith(mockDataFullName, mockLeafKeys) + expect(mockUpdateStatusSelected).toBeCalledWith(mockData.nameBuffer) expect(mockUpdateStatusOpen).toBeCalledWith(mockDataFullName, true) + expect(mockGetMetadata).not.toBeCalled() expect(mockSetOpen).not.toBeCalled() }) + it('name, ttl and size should be rendered', () => { + const { getByTestId } = render() + + expect(getByTestId(`node-item_${mockDataFullName}`)).toBeInTheDocument() + expect(getByTestId(`badge-${mockedDataWithMetadata.type}_${mockDataFullName}`)).toBeInTheDocument() + expect(getByTestId(`ttl-${mockDataFullName}`)).toBeInTheDocument() + expect(getByTestId(`size-${mockDataFullName}`)).toBeInTheDocument() + }) + it('"updateStatusOpen", "setOpen" should be called after click on Node', () => { - const mockSetItems = jest.fn() const mockUpdateStatusSelected = jest.fn() const mockUpdateStatusOpen = jest.fn() const mockSetOpen = jest.fn() @@ -93,8 +129,6 @@ describe('Node', () => { ...mockedData, isLeaf: mockIsOpen, fullName: mockDataFullName, - keys: mockLeafKeys, - setItems: mockSetItems, updateStatusSelected: mockUpdateStatusSelected, updateStatusOpen: mockUpdateStatusOpen, } @@ -108,7 +142,6 @@ describe('Node', () => { screen.getByTestId(`node-item_${mockDataFullName}`).click() - expect(mockSetItems).not.toBeCalled() expect(mockUpdateStatusSelected).not.toBeCalled() expect(mockUpdateStatusOpen).toHaveBeenCalledWith(mockDataFullName, !mockIsOpen) expect(mockSetOpen).toBeCalledWith(!mockIsOpen) diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx new file mode 100644 index 0000000000..597fe86359 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx @@ -0,0 +1,190 @@ +import React, { useEffect, useState } from 'react' +import { NodePublicState } from 'react-vtree/dist/es/Tree' +import cx from 'classnames' +import { + EuiIcon, + EuiToolTip, + keys as ElasticKeys, +} from '@elastic/eui' + +import { + Maybe, +} from 'uiSrc/utils' +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import KeyRowTTL from 'uiSrc/pages/browser/components/key-row-ttl' +import KeyRowSize from 'uiSrc/pages/browser/components/key-row-size' +import KeyRowName from 'uiSrc/pages/browser/components/key-row-name' +import KeyRowType from 'uiSrc/pages/browser/components/key-row-type' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { TreeData } from '../../interfaces' +import styles from './styles.module.scss' + +const MAX_NESTING_LEVEL = 20 + +// Node component receives all the data we created in the `treeWalker` + +// internal openness state (`isOpen`), function to change internal openness +// `style` parameter that should be added to the root div. +const Node = ({ + data, + isOpen, + index, + style, + setOpen, +}: NodePublicState) => { + const { + id: nodeId, + isLeaf, + keyCount, + nestingLevel, + fullName, + nameBuffer, + path, + type, + ttl, + shortName, + size, + deleting, + nameString, + keyApproximate, + isSelected, + getMetadata, + onDelete, + onDeleteClicked, + updateStatusOpen, + updateStatusSelected, + } = data + + const [deletePopoverId, setDeletePopoverId] = useState>(undefined) + + useEffect(() => { + if (!isLeaf || !nameBuffer) { + return + } + if (!size || !ttl) { + getMetadata?.(nameBuffer, path) + } + }, []) + + const handleClick = () => { + if (isLeaf) { + updateStatusSelected?.(nameBuffer) + } + + updateStatusOpen?.(fullName, !isOpen) + !isLeaf && setOpen(!isOpen) + } + + const handleKeyDown = ({ key }: React.KeyboardEvent) => { + if (key === ElasticKeys.SPACE) { + handleClick() + } + } + + const handleDelete = (nameBuffer: RedisResponseBuffer) => { + onDelete(nameBuffer) + setDeletePopoverId(undefined) + } + + const handleDeletePopoverOpen = (index: Maybe, type: KeyTypes | ModulesKeyTypes) => { + if (index !== deletePopoverId) { + onDeleteClicked(type) + } + setDeletePopoverId(index !== deletePopoverId ? index : undefined) + } + + const Folder = () => ( + + <> +
+ + + + {nameString} + +
+
+
+ {keyApproximate ? `${keyApproximate < 1 ? '<1' : Math.round(keyApproximate)}%` : '' } +
+
{keyCount ?? ''}
+
+ +
+ ) + + const Leaf = () => ( + <> + + + + + + ) + + const Node = ( +
{}} + data-testid={`node-item_${fullName}${isOpen && !isLeaf ? '--expanded' : ''}`} + > + {!isLeaf && } + {isLeaf && } +
+ ) + + const tooltipContent = ( + <> + {`${fullName}*`} +
+ {`${keyCount} key(s) (${Math.round(keyApproximate * 100) / 100}%)`} + + ) + + return ( +
MAX_NESTING_LEVEL ? MAX_NESTING_LEVEL : nestingLevel) * 8, + }} + className={cx( + styles.nodeContainer, { + [styles.nodeSelected]: isSelected && isLeaf, + [styles.nodeRowEven]: index % 2 === 0, + } + )} + > + {Node} +
+ ) +} + +export default Node diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/index.ts b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/index.ts similarity index 100% rename from redisinsight/ui/src/components/virtual-tree/components/Node/index.ts rename to redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/index.ts diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss new file mode 100644 index 0000000000..dd3be8344d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss @@ -0,0 +1,142 @@ +.anchorTooltipNode { + width: 100%; + height: 42px; + display: flex !important; + position: relative; + align-items: center; +} + +.nodeContainer { + border-left: 3px solid transparent; + + &:hover { + background-color: var(--browserComponentActive); + } +} + +.nodeRowEven { + background-color: var(--browserTableRowEven); +} + +.nodeContent { + display: flex; + justify-content: space-between; + cursor: pointer; + padding: 8px 16px; + color: var(--euiTextSubduedColor) !important; + align-items: center; + font: + normal normal normal 13px/28px Graphik, + sans-serif !important; + letter-spacing: -0.13px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + height: 100%; + + &Open { + color: var(--euiColorFullShade) !important; + } + + :global(.moveOnHoverKey) { + transition: transform ease 0.3s; + &:global(.hide) { + transform: translateX(-8px); + } + } + :global(.showOnHoverKey) { + display: none !important; + &:global(.show) { + display: flex !important; + } + } + + &:hover { + :global(.moveOnHoverKey) { + transform: translateX(-8px); + } + :global(.showOnHoverKey) { + display: flex !important; + } + } +} + +.nodeSelected { + border-left-color: var(--euiColorPrimary) !important; + background-color: var(--browserComponentActive) !important; + + .nodeContent { + color: var(--euiColorFullShade) !important; + } +} + +.nodeIcon { + margin-right: 8px; + + &Arrow { + margin-left: 8px; + margin-right: 6px; + width: 10px !important; + height: 10px !important; + } + + &Leaf { + margin-left: 25px; + margin-bottom: 2px; + width: 14px; + height: 14px; + } +} + +.approximate, +.keyCount { + display: inline-block; +} + +.options { + padding-left: 12px; + font-size: 12px; + font-weight: 300; +} + +.keyType { + padding-right: 16px; + padding-left: 12px; + width: 126px; + min-width: 126px; +} + +.keyName, +.nodeName { + flex-grow: 1; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + + :global(.euiTextColor) { + max-width: 100%; + } +} + +.keyTTL, +.approximate { + width: 86px; + min-width: 86px; + text-align: right; +} + +.keySize, +.keyCount { + width: 90px; + min-width: 90px; + text-align: right; +} + +.keyInfoLoading { + margin-top: 8px; + padding-left: 16px; +} + +.deletePopover { + max-width: 400px !important; +} diff --git a/redisinsight/ui/src/components/virtual-tree/index.ts b/redisinsight/ui/src/pages/browser/components/virtual-tree/index.ts similarity index 100% rename from redisinsight/ui/src/components/virtual-tree/index.ts rename to redisinsight/ui/src/pages/browser/components/virtual-tree/index.ts diff --git a/redisinsight/ui/src/components/virtual-tree/interfaces.ts b/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts similarity index 63% rename from redisinsight/ui/src/components/virtual-tree/interfaces.ts rename to redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts index d2b1c21d26..119c843d29 100644 --- a/redisinsight/ui/src/components/virtual-tree/interfaces.ts +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts @@ -1,5 +1,6 @@ import { FixedSizeNodeData } from 'react-vtree' -import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces' export interface TreeNode { children: TreeNode[] @@ -24,12 +25,10 @@ export interface NodeMetaData { keyCount: number, name: string, fullName: string, - setItems: (keys: any[]) => void, updateStatusSelected: (fullName: string, keys: any) => void, updateStatusOpen: (name: string, value: boolean) => void, leafIcon: string, keyApproximate: number, - keys: any, isSelected: boolean, isOpenByDefault: boolean, } @@ -37,14 +36,24 @@ export interface NodeMetaData { export interface TreeData extends FixedSizeNodeData { isLeaf: boolean name: string + nameString: string + nameBuffer: RedisResponseBuffer + path: string keyCount: number keyApproximate: number fullName: string + shortName?: string leafIcon: string - keys: IKeyPropTypes[] + type: KeyTypes | ModulesKeyTypes + ttl: number + size: number nestingLevel: number + deleting: boolean isSelected: boolean - setItems: (keys: any[]) => void + children?: TreeData[] updateStatusOpen: (fullName: string, value: boolean) => void - updateStatusSelected: (fullName: string, keys: IKeyPropTypes[]) => void + updateStatusSelected: (key: RedisString) => void + getMetadata: (key: RedisString, path: string) => void + onDelete: (key: RedisResponseBuffer) => void + onDeleteClicked: (type: KeyTypes | ModulesKeyTypes) => void } diff --git a/redisinsight/ui/src/components/virtual-tree/styles.module.scss b/redisinsight/ui/src/pages/browser/components/virtual-tree/styles.module.scss similarity index 95% rename from redisinsight/ui/src/components/virtual-tree/styles.module.scss rename to redisinsight/ui/src/pages/browser/components/virtual-tree/styles.module.scss index 88595be6ce..64ec904730 100644 --- a/redisinsight/ui/src/components/virtual-tree/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/styles.module.scss @@ -36,3 +36,7 @@ top: 12px; left: 12px; } + +.progress { + z-index: 2; +} diff --git a/redisinsight/ui/src/pages/browser/modules/index.ts b/redisinsight/ui/src/pages/browser/modules/index.ts new file mode 100644 index 0000000000..eaff655937 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/index.ts @@ -0,0 +1,3 @@ +export { KeyDetails } from './key-details' +export { KeyDetailsHeader } from './key-details-header' +export type { KeyDetailsHeaderProps } from './key-details-header' diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.spec.tsx similarity index 53% rename from redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.spec.tsx index 495b4e26fb..87db0c037e 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/KeyDetailsHeader.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.spec.tsx @@ -1,16 +1,25 @@ import React from 'react' import { mock } from 'ts-mockito' -import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' -import { stringDataSelector } from 'uiSrc/slices/browser/string' +import { cloneDeep } from 'lodash' +import { render, screen, fireEvent, mockedStore, cleanup } from 'uiSrc/utils/test-utils' import { KeyTypes } from 'uiSrc/constants' -import KeyDetailsHeader, { Props } from './KeyDetailsHeader' +import { deleteSelectedKey } from 'uiSrc/slices/browser/keys' +import { KeyDetailsHeaderProps, KeyDetailsHeader } from './KeyDetailsHeader' -const mockedProps = mock() +const mockedProps = mock() const KEY_INPUT_TEST_ID = 'edit-key-input' const KEY_BTN_TEST_ID = 'edit-key-btn' const TTL_INPUT_TEST_ID = 'edit-ttl-input' -const EDIT_VALUE_BTN_TEST_ID = 'edit-key-value-btn' +const DELETE_KEY_BTN_TEST_ID = 'delete-key-btn' +const DELETE_KEY_CONFIRM_BTN_TEST_ID = 'delete-key-confirm-btn' + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) jest.mock('uiSrc/slices/browser/string', () => ({ ...jest.requireActual('uiSrc/slices/browser/string'), @@ -71,29 +80,6 @@ describe('KeyDetailsHeader', () => { fireEvent.click(screen.getByLabelText(/Copy key name/i)) }) - it('should call onRefresh', () => { - const onRefresh = jest.fn() - render() - - fireEvent.click(screen.getByTestId('refresh-key-btn')) - expect(onRefresh).toBeCalled() - }) - - it('should call onEditKey', () => { - const onEditKey = jest.fn() - render() - - fireEvent.click(screen.getByTestId(KEY_BTN_TEST_ID)) - - fireEvent.change( - screen.getByTestId(KEY_INPUT_TEST_ID), - { target: { value: 'key' } } - ) - - fireEvent.click(screen.getByTestId('apply-btn')) - expect(onEditKey).toBeCalled() - }) - it('should change ttl properly', () => { render() @@ -107,35 +93,24 @@ describe('KeyDetailsHeader', () => { expect(screen.getByTestId(TTL_INPUT_TEST_ID)).toHaveValue('100') }) - it('should be able to change value (long string fully load)', () => { - render( - - ) - - const editValueBtn = screen.getByTestId(`${EDIT_VALUE_BTN_TEST_ID}`) - expect(editValueBtn).toHaveProperty('disabled', false) - }) - - it('should not be able to change value (long string not fully load)', () => { - const stringDataSelectorMock = jest.fn().mockReturnValue({ - value: { - type: 'Buffer', - data: [49, 50, 51], - } + describe('should call onRefresh', () => { + test.each(Object.values(KeyTypes))('should call onRefresh for keyType: %s', (keyType) => { + const component = render() + fireEvent.click(screen.getByTestId('refresh-key-btn')) + expect(component).toBeTruthy() }) - stringDataSelector.mockImplementation(stringDataSelectorMock) + }) - render( - - ) + describe('should call onDelete', () => { + test.each(Object.values(KeyTypes))('should call onDelete for keyType: %s', (keyType) => { + const onRemoveKeyMock = jest.fn() + const component = render() + fireEvent.click(screen.getByTestId(DELETE_KEY_BTN_TEST_ID)) + fireEvent.click(screen.getByTestId(DELETE_KEY_CONFIRM_BTN_TEST_ID)) + expect(component).toBeTruthy() - const editValueBtn = screen.getByTestId(`${EDIT_VALUE_BTN_TEST_ID}`) - expect(editValueBtn).toHaveProperty('disabled', true) + const expectedActions = [deleteSelectedKey()] + expect(store.getActions()).toEqual(expect.arrayContaining(expectedActions)) + }) }) }) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.tsx new file mode 100644 index 0000000000..68e7a62130 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.tsx @@ -0,0 +1,197 @@ +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiToolTip, +} from '@elastic/eui' +import React, { ReactElement } from 'react' +import { isUndefined } from 'lodash' +import { useDispatch, useSelector } from 'react-redux' +import AutoSizer from 'react-virtualized-auto-sizer' + +import { GroupBadge, AutoRefresh, FullScreen } from 'uiSrc/components' +import { + HIDE_LAST_REFRESH, + KeyTypes, + ModulesKeyTypes, +} from 'uiSrc/constants' +import { + deleteSelectedKeyAction, + editKey, + editKeyTTL, + initialKeyInfo, + keysSelector, + refreshKey, + selectedKeyDataSelector, + selectedKeySelector, +} from 'uiSrc/slices/browser/keys' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' + +import { resetStringValue } from 'uiSrc/slices/browser/string' +import { Nullable } from 'uiSrc/utils' +import { KeyDetailsHeaderFormatter } from './components/key-details-header-formatter' +import { KeyDetailsHeaderName } from './components/key-details-header-name' +import { KeyDetailsHeaderTTL } from './components/key-details-header-ttl' +import { KeyDetailsHeaderDelete } from './components/key-details-header-delete' +import { KeyDetailsHeaderSizeLength } from './components/key-details-header-size-length' + +import styles from './styles.module.scss' + +export interface KeyDetailsHeaderProps { + keyType: KeyTypes | ModulesKeyTypes + onCloseKey: (key: RedisResponseBuffer) => void + onRemoveKey: () => void + onEditKey: (key: RedisResponseBuffer, newKey: RedisResponseBuffer, onFailure?: () => void) => void + isFullScreen: boolean + arePanelsCollapsed: boolean + onToggleFullScreen: () => void + Actions?: (props: { width: number }) => ReactElement +} + +const KeyDetailsHeader = ({ + isFullScreen, + arePanelsCollapsed, + onToggleFullScreen = () => {}, + onCloseKey, + onRemoveKey, + onEditKey, + keyType, + Actions, +}: KeyDetailsHeaderProps) => { + const { loading, lastRefreshTime } = useSelector(selectedKeySelector) + const { + type, + length, + nameString: keyProp, + name: keyBuffer, + } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo + const { id: instanceId } = useSelector(connectedInstanceSelector) + const { viewType } = useSelector(keysSelector) + + const dispatch = useDispatch() + + const handleRefreshKey = () => { + dispatch(refreshKey(keyBuffer!, type)) + } + + const handleEditTTL = (key: RedisResponseBuffer, ttl: number) => { + dispatch(editKeyTTL(key, ttl)) + } + const handleEditKey = (oldKey: RedisResponseBuffer, newKey: RedisResponseBuffer, onFailure?: () => void) => { + dispatch(editKey(oldKey, newKey, () => onEditKey(oldKey, newKey), onFailure)) + } + + const handleDeleteKey = (key: RedisResponseBuffer) => { + dispatch(deleteSelectedKeyAction(key, onRemoveKey)) + } + + const handleEnableAutoRefresh = (enableAutoRefresh: boolean, refreshRate: string) => { + const browserViewEvent = enableAutoRefresh + ? TelemetryEvent.BROWSER_KEY_DETAILS_AUTO_REFRESH_ENABLED + : TelemetryEvent.BROWSER_KEY_DETAILS_AUTO_REFRESH_DISABLED + const treeViewEvent = enableAutoRefresh + ? TelemetryEvent.TREE_VIEW_KEY_DETAILS_AUTO_REFRESH_ENABLED + : TelemetryEvent.TREE_VIEW_KEY_DETAILS_AUTO_REFRESH_DISABLED + sendEventTelemetry({ + event: getBasedOnViewTypeEvent(viewType, browserViewEvent, treeViewEvent), + eventData: { + length, + databaseId: instanceId, + keyType: type, + refreshRate: +refreshRate + } + }) + } + + const handleChangeAutoRefreshRate = (enableAutoRefresh: boolean, refreshRate: string) => { + if (enableAutoRefresh) { + handleEnableAutoRefresh(enableAutoRefresh, refreshRate) + } + } + + return ( +
+ {loading ? ( +
+ +
+ ) : ( + + {({ width = 0 }) => ( +
+ + + + + + + {!arePanelsCollapsed && ( + + + + )} + + {(!arePanelsCollapsed || isFullScreen) && ( + + onCloseKey(keyProp)} + data-testid="close-key-btn" + /> + + )} + + + + + + +
+ HIDE_LAST_REFRESH} + containerClassName={styles.actionBtn} + onRefresh={handleRefreshKey} + onEnableAutoRefresh={handleEnableAutoRefresh} + onChangeAutoRefreshRate={handleChangeAutoRefreshRate} + testid="refresh-key-btn" + /> + {Object.values(KeyTypes).includes(keyType as KeyTypes) && ( + + )} + {!isUndefined(Actions) && } + +
+
+
+
+ )} +
+ )} +
+ ) +} + +export { KeyDetailsHeader } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/KeyDetailsHeaderDelete.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/KeyDetailsHeaderDelete.spec.tsx new file mode 100644 index 0000000000..0706d8fb9a --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/KeyDetailsHeaderDelete.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, KeyDetailsHeaderDelete } from './KeyDetailsHeaderDelete' + +const mockedProps = mock() + +describe('KeyDetailsHeaderDelete', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/KeyDetailsHeaderDelete.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/KeyDetailsHeaderDelete.tsx new file mode 100644 index 0000000000..b052b788d8 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/KeyDetailsHeaderDelete.tsx @@ -0,0 +1,103 @@ +import { + EuiButton, + EuiButtonIcon, + EuiPopover, + EuiText, +} from '@elastic/eui' +import React, { useState } from 'react' +import { useSelector } from 'react-redux' + +import { initialKeyInfo, keysSelector, selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { + formatLongName, +} from 'uiSrc/utils' + +import styles from './styles.module.scss' + +export interface Props { + onDelete: (key: RedisResponseBuffer) => void +} + +const KeyDetailsHeaderDelete = ({ onDelete }: Props) => { + const { + type, + nameString: keyProp, + name: keyBuffer, + } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo + const { id: instanceId } = useSelector(connectedInstanceSelector) + const { viewType } = useSelector(keysSelector) + + const [isPopoverDeleteOpen, setIsPopoverDeleteOpen] = useState(false) + + const tooltipContent = formatLongName(keyProp || '') + + const closePopoverDelete = () => { + setIsPopoverDeleteOpen(false) + } + + const showPopoverDelete = () => { + setIsPopoverDeleteOpen((isPopoverDeleteOpen) => !isPopoverDeleteOpen) + sendEventTelemetry({ + event: getBasedOnViewTypeEvent( + viewType, + TelemetryEvent.BROWSER_KEY_DELETE_CLICKED, + TelemetryEvent.TREE_VIEW_KEY_DELETE_CLICKED + ), + eventData: { + databaseId: instanceId, + source: 'keyValue', + keyType: type + } + }) + } + + return ( + + )} + > +
+ +

+ {tooltipContent} +

+ + will be deleted. + +
+
+ onDelete(keyBuffer)} + className={styles.popoverDeleteBtn} + data-testid="delete-key-confirm-btn" + > + Delete + +
+
+
+ ) +} + +export { KeyDetailsHeaderDelete } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/index.ts new file mode 100644 index 0000000000..75dc9271f7 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/index.ts @@ -0,0 +1 @@ +export { KeyDetailsHeaderDelete } from './KeyDetailsHeaderDelete' diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/styles.module.scss new file mode 100644 index 0000000000..3a8e0eee1f --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/styles.module.scss @@ -0,0 +1,8 @@ +.popoverDeleteContainer { + overflow: hidden; + max-width: 350px !important; +} + +.popoverFooter { + margin-top: 10px; +} diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.spec.tsx similarity index 77% rename from redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.spec.tsx index 2d53a1ac6c..41f250fd55 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.spec.tsx @@ -2,7 +2,7 @@ import React from 'react' import { mock } from 'ts-mockito' import { fireEvent, render, screen, waitForEuiPopoverVisible } from 'uiSrc/utils/test-utils' -import KeyValueFormatter, { Props } from './KeyValueFormatter' +import { Props, KeyDetailsHeaderFormatter } from './KeyDetailsHeaderFormatter' const mockedProps = { ...mock(), @@ -10,7 +10,7 @@ const mockedProps = { describe('KeyValueFormatter', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) it('should render options in the strict order', async () => { @@ -26,7 +26,7 @@ describe('KeyValueFormatter', () => { 'PHP serialized', 'Java serialized', ] - render() + render() fireEvent.click(screen.getByTestId('select-format-key-value')) diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.tsx similarity index 94% rename from redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.tsx index 049e58f92c..a693b53814 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.tsx @@ -4,7 +4,7 @@ import { EuiIcon, EuiSuperSelect, EuiSuperSelectOption, EuiText, EuiTextColor, E import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' -import { KeyTypes, KeyValueFormat, TEXT_DISABLED_STRING_FORMATTING, Theme } from 'uiSrc/constants' +import { KeyTypes, KeyValueFormat, MIDDLE_SCREEN_RESOLUTION, TEXT_DISABLED_STRING_FORMATTING, Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { keysSelector, selectedKeyDataSelector, selectedKeySelector, setViewFormat } from 'uiSrc/slices/browser/keys' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' @@ -13,13 +13,12 @@ import FormattersDark from 'uiSrc/assets/img/icons/formatter_dark.svg' import { stringDataSelector } from 'uiSrc/slices/browser/string' import { isFullStringLoaded } from 'uiSrc/utils' import { getKeyValueFormatterOptions } from './constants' -import { MIDDLE_SCREEN_RESOLUTION } from '../../KeyDetailsHeader' import styles from './styles.module.scss' export interface Props { width: number } -const KeyValueFormatter = (props: Props) => { +const KeyDetailsHeaderFormatter = (props: Props) => { const { width } = props const { instanceId = '' } = useParams<{ instanceId: string }>() @@ -114,4 +113,4 @@ const KeyValueFormatter = (props: Props) => { ) } -export default KeyValueFormatter +export { KeyDetailsHeaderFormatter } diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/constants.ts b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/constants.ts similarity index 100% rename from redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/constants.ts rename to redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/constants.ts diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/index.ts new file mode 100644 index 0000000000..4cc693fb9b --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/index.ts @@ -0,0 +1 @@ +export { KeyDetailsHeaderFormatter } from './KeyDetailsHeaderFormatter' diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.spec.tsx new file mode 100644 index 0000000000..70d3cd3373 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, KeyDetailsHeaderName } from './KeyDetailsHeaderName' + +const mockedProps = mock() + +describe('KeyDetailsHeaderName', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.tsx new file mode 100644 index 0000000000..09f99a0cae --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.tsx @@ -0,0 +1,228 @@ +import { + EuiButtonIcon, + EuiFieldText, + EuiFlexGrid, + EuiFlexItem, + EuiIcon, + EuiText, + EuiToolTip, +} from '@elastic/eui' +import cx from 'classnames' +import { isNull } from 'lodash' +import React, { ChangeEvent, useEffect, useRef, useState } from 'react' +import { useSelector } from 'react-redux' + +import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' +import { + TEXT_UNPRINTABLE_CHARACTERS, +} from 'uiSrc/constants' +import { AddCommonFieldsFormConfig } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' +import { initialKeyInfo, keysSelector, selectedKeyDataSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { + formatLongName, + isEqualBuffers, + replaceSpaces, + stringToBuffer, +} from 'uiSrc/utils' + +import styles from './styles.module.scss' + +export interface Props { + onEditKey: (key: RedisResponseBuffer, newKey: RedisResponseBuffer, onFailure?: () => void) => void +} + +const COPY_KEY_NAME_ICON = 'copyKeyNameIcon' + +const KeyDetailsHeaderName = ({ + onEditKey, +}: Props) => { + const { loading } = useSelector(selectedKeySelector) + const { + ttl: ttlProp, + type, + nameString: keyProp, + name: keyBuffer, + } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo + const { id: instanceId } = useSelector(connectedInstanceSelector) + const { viewType } = useSelector(keysSelector) + + const [key, setKey] = useState(keyProp) + const [keyIsEditing, setKeyIsEditing] = useState(false) + const [keyIsHovering, setKeyIsHovering] = useState(false) + const [keyIsEditable, setKeyIsEditable] = useState(true) + + useEffect(() => { + setKey(keyProp) + setKeyIsEditable(isEqualBuffers(keyBuffer, stringToBuffer(keyProp || ''))) + }, [keyProp, ttlProp, keyBuffer]) + + const keyNameRef = useRef(null) + + const tooltipContent = formatLongName(keyProp || '') + + const onMouseEnterKey = () => { + setKeyIsHovering(true) + } + + const onMouseLeaveKey = () => { + setKeyIsHovering(false) + } + + const onClickKey = () => { + setKeyIsEditing(true) + } + + const onChangeKey = ({ currentTarget: { value } }: ChangeEvent) => { + keyIsEditing && setKey(value) + } + + const applyEditKey = () => { + setKeyIsEditing(false) + setKeyIsHovering(false) + + const newKeyBuffer = stringToBuffer(key || '') + + if (keyBuffer && !isEqualBuffers(keyBuffer, newKeyBuffer) && !isNull(keyProp)) { + onEditKey(keyBuffer, newKeyBuffer, () => setKey(keyProp)) + } + } + + const cancelEditKey = (event?: React.MouseEvent) => { + const { id } = event?.target as HTMLElement || {} + if (id === COPY_KEY_NAME_ICON) { + return + } + setKey(keyProp) + setKeyIsEditing(false) + setKeyIsHovering(false) + + event?.stopPropagation() + } + + const handleCopy = ( + event: any, + text = '', + keyInputIsEditing: boolean, + keyNameInputRef: React.RefObject + ) => { + navigator.clipboard.writeText(text) + + if (keyInputIsEditing) { + keyNameInputRef?.current?.focus() + } + + event.stopPropagation() + + sendEventTelemetry({ + event: getBasedOnViewTypeEvent( + viewType, + TelemetryEvent.BROWSER_KEY_COPIED, + TelemetryEvent.TREE_VIEW_KEY_COPIED + ), + eventData: { + databaseId: instanceId, + keyType: type + } + }) + } + + const appendKeyEditing = () => + (!keyIsEditing ? : '') + + return ( + + {(keyIsEditing || keyIsHovering) && ( + + + + <> + applyEditKey()} + isDisabled={!keyIsEditable} + disabledTooltipText={TEXT_UNPRINTABLE_CHARACTERS} + onDecline={(event) => cancelEditKey(event)} + viewChildrenMode={!keyIsEditing} + isLoading={loading} + declineOnUnmount={false} + > + + +

{key}

+ +
+ {keyIsHovering && ( + + + handleCopy(event, key!, keyIsEditing, keyNameRef)} + data-testid="copy-key-name-btn" + /> + + )} +
+
+ )} + + + {replaceSpaces(keyProp?.substring(0, 200))} + + +
+ + ) +} + +export { KeyDetailsHeaderName } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/index.ts new file mode 100644 index 0000000000..f5d9e23051 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/index.ts @@ -0,0 +1 @@ +export { KeyDetailsHeaderName } from './KeyDetailsHeaderName' diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/styles.module.scss new file mode 100644 index 0000000000..72e267185d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/styles.module.scss @@ -0,0 +1,69 @@ +.classNameGridComponent { + position: relative; +} + +.flexItemKeyInput { + flex-direction: row !important; + width: 100% !important; +} + +.toolTipAnchorKey { + max-width: calc(100% - 25px); + height: 31px !important; +} + +.keyInput { + height: 31px !important; + font-size: 14px !important; + font-weight: 500 !important; +} + +:global(.browserPage .key-details-header .euiFormControlLayout) { + .keyInputEditing { + height: 31px !important; + } +} + +.keyHiddenText { + display: inline-block; + visibility: hidden; + height: 1px; + overflow: hidden; + max-width: 100%; + margin-right: 80px; + word-break: break-all; +} + +.copyKey { + position: absolute; + padding-left: 7px; + padding-top: 4px; + right: 0; + height: 31px; + width: 25px; +} + +.capitalize { + text-transform: capitalize; +} + +.key { + display: flex; + width: 100%; + min-width: 100%; + + padding-left: 9px; + line-height: 31px !important; +} + +.hidden { + display: none; +} + +.keyFlexItem { + overflow: hidden; +} + +.keyFlexItemEditing { + overflow: inherit; +} diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/KeyDetailsHeaderSizeLength.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/KeyDetailsHeaderSizeLength.spec.tsx new file mode 100644 index 0000000000..6d9471e304 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/KeyDetailsHeaderSizeLength.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, KeyDetailsHeaderSizeLength } from './KeyDetailsHeaderSizeLength' + +const mockedProps = mock() + +describe('KeyDetailsHeaderSizeLength', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/KeyDetailsHeaderSizeLength.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/KeyDetailsHeaderSizeLength.tsx new file mode 100644 index 0000000000..37fa8eb94e --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/KeyDetailsHeaderSizeLength.tsx @@ -0,0 +1,73 @@ +import { + EuiFlexItem, + EuiText, + EuiToolTip, +} from '@elastic/eui' +import React from 'react' +import { useSelector } from 'react-redux' + +import { LENGTH_NAMING_BY_TYPE, MIDDLE_SCREEN_RESOLUTION } from 'uiSrc/constants' +import { initialKeyInfo, selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' +import { formatBytes } from 'uiSrc/utils' + +import styles from './styles.module.scss' + +export interface Props { + width: number +} + +const KeyDetailsHeaderSizeLength = ({ + width, +}: Props) => { + const { + type, + size, + length, + } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo + + return ( + <> + {size && ( + + + + {formatBytes(size, 3)} + + )} + > + <> + {width > MIDDLE_SCREEN_RESOLUTION && 'Key Size: '} + {formatBytes(size, 0)} + + + + + )} + + + {LENGTH_NAMING_BY_TYPE[type] ?? 'Length'} + {': '} + {length ?? '-'} + + + + ) +} + +export { KeyDetailsHeaderSizeLength } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/index.ts new file mode 100644 index 0000000000..36681672cd --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/index.ts @@ -0,0 +1 @@ +export { KeyDetailsHeaderSizeLength } from './KeyDetailsHeaderSizeLength' diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/styles.module.scss new file mode 100644 index 0000000000..327aba3dbf --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/styles.module.scss @@ -0,0 +1,4 @@ +.subtitleText { + padding: 6px 2px 6px 0; +} + diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/KeyDetailsHeaderTTL.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/KeyDetailsHeaderTTL.spec.tsx new file mode 100644 index 0000000000..81e9f7aa66 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/KeyDetailsHeaderTTL.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, KeyDetailsHeaderTTL } from './KeyDetailsHeaderTTL' + +const mockedProps = mock() + +describe('KeyDetailsHeaderTTL', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/KeyDetailsHeaderTTL.tsx b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/KeyDetailsHeaderTTL.tsx new file mode 100644 index 0000000000..dd7cf1baf6 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/KeyDetailsHeaderTTL.tsx @@ -0,0 +1,160 @@ +import { + EuiFieldText, + EuiFlexGrid, + EuiFlexItem, + EuiIcon, + EuiText, +} from '@elastic/eui' +import cx from 'classnames' +import React, { ChangeEvent, useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + +import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' +import { initialKeyInfo, selectedKeyDataSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { + + MAX_TTL_NUMBER, + validateTTLNumber +} from 'uiSrc/utils' + +import styles from './styles.module.scss' + +export interface Props { + onEditTTL: (key: RedisResponseBuffer, ttl: number) => void +} + +const KeyDetailsHeaderTTL = ({ + onEditTTL, +}: Props) => { + const { loading } = useSelector(selectedKeySelector) + const { + ttl: ttlProp, + nameString: keyProp, + name: keyBuffer, + } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo + + const [ttl, setTTL] = useState(`${ttlProp}`) + const [ttlIsEditing, setTTLIsEditing] = useState(false) + const [ttlIsHovering, setTTLIsHovering] = useState(false) + + useEffect(() => { + setTTL(`${ttlProp}`) + }, [keyProp, ttlProp, keyBuffer]) + + const onMouseEnterTTL = () => { + setTTLIsHovering(true) + } + + const onMouseLeaveTTL = () => { + setTTLIsHovering(false) + } + + const onClickTTL = () => { + setTTLIsEditing(true) + } + + const onChangeTtl = ({ currentTarget: { value } }: ChangeEvent) => { + ttlIsEditing && setTTL(validateTTLNumber(value) || '-1') + } + + const applyEditTTL = () => { + const ttlValue = ttl || '-1' + + setTTLIsEditing(false) + setTTLIsHovering(false) + + if (`${ttlProp}` !== ttlValue && keyBuffer) { + onEditTTL(keyBuffer, +ttlValue) + } + } + + const cancelEditTTl = (event: any) => { + setTTL(`${ttlProp}`) + setTTLIsEditing(false) + setTTLIsHovering(false) + + event?.stopPropagation() + } + + const appendTTLEditing = () => + (!ttlIsEditing ? : '') + + return ( + + + <> + {(ttlIsEditing || ttlIsHovering) && ( + + + + TTL: + + + + applyEditTTL()} + onDecline={(event) => cancelEditTTl(event)} + viewChildrenMode={!ttlIsEditing} + isLoading={loading} + declineOnUnmount={false} + > + + + + + )} + + TTL: + + {ttl === '-1' ? 'No limit' : ttl} + + + + + ) +} + +export { KeyDetailsHeaderTTL } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/index.ts new file mode 100644 index 0000000000..1917ea4162 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/index.ts @@ -0,0 +1 @@ +export { KeyDetailsHeaderTTL } from './KeyDetailsHeaderTTL' diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/styles.module.scss new file mode 100644 index 0000000000..64aa90f993 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/styles.module.scss @@ -0,0 +1,43 @@ +.subtitleText { + padding: 6px 2px 6px 0; +} + +.controlsKey { + right: 25px; +} + + + +.cancelEditBtn:hover { + color: var(--euiColorColorDanger) !important; +} + +.applyEditBtn:hover { + color: var(--euiColorSecondary) !important; +} + +.flexItemTTL { + width: 152px; + min-width: 152px; +} + +.ttlInput { + min-width: 106px; + font-size: 13px !important; + &.editing { + width: 124px; + } +} + +.ttlGridComponent, +.classNameGridComponent { + position: relative; +} + +.hidden { + display: none; +} + +.ttlTextValue { + padding-left: 11px; +} diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details-header/index.ts new file mode 100644 index 0000000000..f5a0b7c62d --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/index.ts @@ -0,0 +1,2 @@ +export { KeyDetailsHeader } from './KeyDetailsHeader' +export type { KeyDetailsHeaderProps } from './KeyDetailsHeader' diff --git a/redisinsight/ui/src/pages/browser/modules/key-details-header/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details-header/styles.module.scss new file mode 100644 index 0000000000..8ef5f5b697 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details-header/styles.module.scss @@ -0,0 +1,64 @@ +:global { + .browserPage { + .key-details-header { + .euiFieldText--compressed, + .euiFormControlLayout--compressed { + height: 29px !important; + } + + .euiFormControlLayout { + width: 100%; + max-width: 100%; + + &.euiFormControlLayout--readOnly { + border: 1px solid var(--controlsBorderColor); + cursor: auto; + } + + input { + height: 29px !important; + cursor: pointer; + max-width: none; + font-family: 'Graphik', sans-serif !important; + } + } + } + } +} + + +.container { + min-height: 108px; + padding: 18px 18px 12px 18px; + border-bottom: 1px solid var(--euiColorLightShade); + min-width: 100%; + position: relative; +} + +.closeBtn { + padding-top: 0 !important; + + svg { + width: 20px; + height: 20px; + } +} + +.groupSecondLine { + margin-top: 4px !important; +} + + +.subtitleActionBtns { + display: flex; + justify-content: flex-end; + align-items: center; + right: 13px; +} + + +.actionBtn { + margin-right: 12px; + position: relative; + z-index: 2; +} diff --git a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.spec.tsx similarity index 90% rename from redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.spec.tsx index 631bc4f69a..0497edf349 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details/KeyDetails/KeyDetails.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.spec.tsx @@ -1,9 +1,10 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { render, screen } from 'uiSrc/utils/test-utils' -import KeyDetails, { Props } from './KeyDetails' -const mockedProps = mock() +import KeyDetails, { Props as KeyDetailsProps } from './KeyDetails' + +const mockedProps = mock() describe('KeyDetails', () => { it('should render', () => { diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.tsx new file mode 100644 index 0000000000..39c82423bd --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.tsx @@ -0,0 +1,131 @@ +import React, { useEffect } from 'react' +import { isNull, isUndefined } from 'lodash' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import cx from 'classnames' + +import { + fetchKeyInfo, + keysSelector, + selectedKeyDataSelector, + selectedKeySelector, +} from 'uiSrc/slices/browser/keys' +import { KeyTypes } from 'uiSrc/constants' + +import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { Nullable } from 'uiSrc/utils' +import { NoKeySelected } from './components/no-key-selected' +import { DynamicTypeDetails } from './components/dynamic-type-details' + +import styles from './styles.module.scss' + +export interface Props { + isFullScreen: boolean + arePanelsCollapsed: boolean + onToggleFullScreen: () => void + onCloseKey: () => void + onEditKey: (key: RedisResponseBuffer, newKey: RedisResponseBuffer) => void + onRemoveKey: () => void + keyProp: RedisResponseBuffer | null + totalKeys: number + keysLastRefreshTime: Nullable +} + +const KeyDetails = (props: Props) => { + const { + onCloseKey, + keyProp, + totalKeys, + keysLastRefreshTime, + } = props + + const { instanceId } = useParams<{ instanceId: string }>() + const { viewType } = useSelector(keysSelector) + const { loading, error = '', data } = useSelector(selectedKeySelector) + const isKeySelected = !isNull(useSelector(selectedKeyDataSelector)) + const { type: keyType, name: keyName, length: keyLength } = useSelector(selectedKeyDataSelector) ?? { + type: KeyTypes.String, + } + + const dispatch = useDispatch() + + useEffect(() => { + if (keyProp === null) { + return + } + // Restore key details from context in future + // (selectedKey.data?.name !== keyProp) + dispatch(fetchKeyInfo(keyProp)) + }, [keyProp]) + + useEffect(() => { + if (!isUndefined(keyName)) { + sendEventTelemetry({ + event: getBasedOnViewTypeEvent( + viewType, + TelemetryEvent.BROWSER_KEY_VALUE_VIEWED, + TelemetryEvent.TREE_VIEW_KEY_VALUE_VIEWED + ), + eventData: { + keyType, + databaseId: instanceId, + length: keyLength, + } + }) + } + }, [keyName]) + + const onCloseAddItemPanel = () => { + sendEventTelemetry({ + event: getBasedOnViewTypeEvent( + viewType, + TelemetryEvent.BROWSER_KEY_ADD_VALUE_CANCELLED, + TelemetryEvent.TREE_VIEW_KEY_ADD_VALUE_CANCELLED, + ), + eventData: { + databaseId: instanceId, + keyType, + } + }) + } + + const onOpenAddItemPanel = () => { + sendEventTelemetry({ + event: getBasedOnViewTypeEvent( + viewType, + TelemetryEvent.BROWSER_KEY_ADD_VALUE_CLICKED, + TelemetryEvent.TREE_VIEW_KEY_ADD_VALUE_CLICKED + ), + eventData: { + databaseId: instanceId, + keyType, + } + }) + } + + return ( +
+
+ {!isKeySelected && !loading ? ( + + ) : ( + + )} +
+
+ ) +} + +export default React.memo(KeyDetails) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/DynamicTypeDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/DynamicTypeDetails.spec.tsx new file mode 100644 index 0000000000..f97fea3641 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/DynamicTypeDetails.spec.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants' +import { Props, DynamicTypeDetails } from './DynamicTypeDetails' + +const mockedProps = mock() + +const DynamicTypeDetailsTypeTests: any[] = [ + [KeyTypes.Hash, 'hash-details'], + [KeyTypes.ZSet, 'zset-details'], + [KeyTypes.Set, 'set-details'], + [KeyTypes.List, 'list-details'], + [KeyTypes.Stream, 'stream-details'], + [KeyTypes.ReJSON, 'json-details'], + [ModulesKeyTypes.Graph, 'modules-type-details'], + [ModulesKeyTypes.TimeSeries, 'modules-type-details'], + ['123', 'unsupported-type-details'], +] + +describe('DynamicTypeDetails', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it.each(DynamicTypeDetailsTypeTests)('for key type: %s (reply), data-subj should exists: %s', + (type: KeyTypes, testId: string) => { + const { queryByTestId } = render() + expect(queryByTestId(testId)).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/DynamicTypeDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/DynamicTypeDetails.tsx new file mode 100644 index 0000000000..e801038b8f --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/DynamicTypeDetails.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import { KeyTypes, MODULES_KEY_TYPES_NAMES, ModulesKeyTypes } from 'uiSrc/constants' +import { KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules' +import ModulesTypeDetails from '../modules-type-details/ModulesTypeDetails' +import UnsupportedTypeDetails from '../unsupported-type-details/UnsupportedTypeDetails' +import { RejsonDetailsWrapper } from '../rejson-details' +import { StringDetails } from '../string-details' +import { ZSetDetails } from '../zset-details' +import { SetDetails } from '../set-details' +import { HashDetails } from '../hash-details' +import { ListDetails } from '../list-details' +import { StreamDetails } from '../stream-details' + +export interface Props extends KeyDetailsHeaderProps { + onOpenAddItemPanel: () => void + onCloseAddItemPanel: () => void +} + +const DynamicTypeDetails = (props: Props) => { + const { keyType: selectedKeyType } = props + + const TypeDetails: any = { + [KeyTypes.ZSet]: , + [KeyTypes.Set]: , + [KeyTypes.String]: , + [KeyTypes.Hash]: , + [KeyTypes.List]: , + [KeyTypes.ReJSON]: , + [KeyTypes.Stream]: , + } + + // Supported key type + if (selectedKeyType && selectedKeyType in TypeDetails) { + return TypeDetails[selectedKeyType] + } + + // Unsupported redis modules key type + if (Object.values(ModulesKeyTypes).includes(selectedKeyType as ModulesKeyTypes)) { + return + } + + // Unsupported key type + if (!(Object.values(KeyTypes).includes(selectedKeyType as KeyTypes)) + && !(Object.values(ModulesKeyTypes).includes(selectedKeyType as ModulesKeyTypes))) { + return + } + + return null +} + +export { DynamicTypeDetails } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/index.ts new file mode 100644 index 0000000000..ab0fa34c7e --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/index.ts @@ -0,0 +1 @@ +export { DynamicTypeDetails } from './DynamicTypeDetails' diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.spec.tsx new file mode 100644 index 0000000000..da72dfa82c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, HashDetails } from './HashDetails' + +const mockedProps = mock() + +describe('HashDetails', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx new file mode 100644 index 0000000000..b28d67cc3b --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx @@ -0,0 +1,67 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' + +import { + selectedKeySelector, +} from 'uiSrc/slices/browser/keys' +import { KeyTypes } from 'uiSrc/constants' + +import { KeyDetailsHeader, KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules' +import { HashDetailsTable } from './hash-details-table' +import AddHashFields from './add-hash-fields/AddHashFields' +import { AddItemsAction } from '../key-details-actions' + +export interface Props extends KeyDetailsHeaderProps { + onRemoveKey: () => void + onOpenAddItemPanel: () => void + onCloseAddItemPanel: () => void +} + +const HashDetails = (props: Props) => { + const keyType = KeyTypes.Hash + const { onRemoveKey, onOpenAddItemPanel, onCloseAddItemPanel } = props + + const { loading } = useSelector(selectedKeySelector) + + const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState(false) + + const openAddItemPanel = () => { + setIsAddItemPanelOpen(true) + onOpenAddItemPanel() + } + + const closeAddItemPanel = () => { + setIsAddItemPanelOpen(false) + onCloseAddItemPanel() + } + + const Actions = ({ width }: { width: number }) => ( + + ) + + return ( +
+ +
+ {!loading && ( +
+ +
+ )} + {isAddItemPanelOpen && ( +
+ +
+ )} +
+
+ ) +} + +export { HashDetails } diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-hash-fields/AddHashFields.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-hash-fields/AddHashFields.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-hash-fields/AddHashFields.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.tsx similarity index 97% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-hash-fields/AddHashFields.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.tsx index deb8e5acc0..ac79648712 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-hash-fields/AddHashFields.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.tsx @@ -139,7 +139,8 @@ const AddHashFields = (props: Props) => { color="transparent" hasShadow={false} borderRadius="none" - className={cx(styles.content, 'eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} + data-test-subj="add-hash-field-panel" + className={cx('eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} > {fields.map((item, index) => ( @@ -188,7 +189,6 @@ const AddHashFields = (props: Props) => { clearItemValues={clearFieldsValues} clearIsDisabled={isClearDisabled(item)} loading={loading} - anchorClassName={styles.refreshKeyTooltip} /> diff --git a/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.spec.tsx similarity index 87% rename from redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.spec.tsx index 51c452a8f1..4a4c841ba3 100644 --- a/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.spec.tsx @@ -7,7 +7,7 @@ import { RedisResponseBufferType } from 'uiSrc/slices/interfaces' import { anyToBuffer, bufferToString } from 'uiSrc/utils' import { act, fireEvent, render, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils' import { GZIP_COMPRESSED_VALUE_1, GZIP_COMPRESSED_VALUE_2, DECOMPRESSED_VALUE_STR_1, DECOMPRESSED_VALUE_STR_2 } from 'uiSrc/utils/tests/decompressors' -import HashDetails, { Props } from './HashDetails' +import { HashDetailsTable, Props } from './HashDetailsTable' const mockedProps = mock() const fields: Array<{ field: RedisResponseBufferType, value: RedisResponseBufferType }> = [ @@ -39,24 +39,24 @@ jest.mock('uiSrc/slices/instances/instances', () => ({ }), })) -describe('HashDetails', () => { +describe('HashDetailsTable', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) it('should render rows properly', () => { - const { container } = render() + const { container } = render() const rows = container.querySelectorAll('.ReactVirtualized__Table__row[role="row"]') expect(rows).toHaveLength(fields.length) }) it('should render search input', () => { - render() + render() expect(screen.getByTestId('search')).toBeTruthy() }) it('should call search', () => { - render() + render() const searchInput = screen.getByTestId('search') fireEvent.change( searchInput, @@ -66,19 +66,19 @@ describe('HashDetails', () => { }) it('should render delete popup after click remove button', () => { - render() + render() fireEvent.click(screen.getAllByTestId(/remove-hash-button/)[0]) expect(screen.getByTestId(`remove-hash-button-${bufferToString(fields[0].field)}-icon`)).toBeInTheDocument() }) it('should render editor after click edit button', () => { - render() + render() fireEvent.click(screen.getAllByTestId(/edit-hash-button/)[0]) expect(screen.getByTestId('hash-value-editor')).toBeInTheDocument() }) it('should render resize trigger for field column', () => { - render() + render() expect(screen.getByTestId('resize-trigger-field')).toBeInTheDocument() }) @@ -95,7 +95,7 @@ describe('HashDetails', () => { }) hashDataSelector.mockImplementation(hashDataSelectorMock) - const { queryByTestId, queryAllByTestId } = render() + const { queryByTestId, queryAllByTestId } = render() const fieldEl = queryAllByTestId(/hash-field-/)?.[0] const valueEl = queryByTestId(/hash-field-value/) @@ -119,7 +119,7 @@ describe('HashDetails', () => { compressor: KeyValueCompressor.GZIP, })) - const { queryByTestId } = render() + const { queryByTestId } = render() const editBtn = queryByTestId(/edit-hash-button/) fireEvent.click(editBtn) diff --git a/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.tsx similarity index 98% rename from redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.tsx index 2401ca6096..4cf7b5a787 100644 --- a/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.tsx @@ -58,7 +58,7 @@ import { stringToBuffer } from 'uiSrc/utils/formatters/bufferFormatters' import { decompressingBuffer } from 'uiSrc/utils/decompressors' import { AddFieldsToHashDto, GetHashFieldsResponse, HashFieldDto, } from 'apiSrc/modules/browser/dto/hash.dto' -import PopoverDelete from '../popover-delete/PopoverDelete' +import PopoverDelete from '../../../../../components/popover-delete/PopoverDelete' import styles from './styles.module.scss' const suffix = '_hash' @@ -78,7 +78,7 @@ export interface Props { onRemoveKey: () => void } -const HashDetails = (props: Props) => { +const HashDetailsTable = (props: Props) => { const { isFooterOpen, onRemoveKey } = props const { @@ -477,6 +477,7 @@ const HashDetails = (props: Props) => { return ( <>
{ ) } -export default HashDetails +export { HashDetailsTable } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/index.ts new file mode 100644 index 0000000000..5caacf2fc2 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/index.ts @@ -0,0 +1 @@ +export { HashDetailsTable } from './HashDetailsTable' diff --git a/redisinsight/ui/src/pages/browser/components/hash-details/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/hash-details/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/index.ts new file mode 100644 index 0000000000..f6b0b8f2e2 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/index.ts @@ -0,0 +1 @@ +export { HashDetails } from './HashDetails' diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/add-items-action/AddItemsAction.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/add-items-action/AddItemsAction.spec.tsx new file mode 100644 index 0000000000..9a94b73691 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/add-items-action/AddItemsAction.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, AddItemsAction } from './AddItemsAction' + +const mockedProps = mock() + +describe('AddItemsAction', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/add-items-action/AddItemsAction.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/add-items-action/AddItemsAction.tsx new file mode 100644 index 0000000000..e09bed67e3 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/add-items-action/AddItemsAction.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import { EuiButton, EuiButtonIcon, EuiToolTip } from '@elastic/eui' +import cx from 'classnames' +import { MIDDLE_SCREEN_RESOLUTION } from 'uiSrc/constants' + +import styles from '../styles.module.scss' + +export interface Props { + width: number + title: string + openAddItemPanel: () => void +} + +const AddItemsAction = ({ width, title, openAddItemPanel }: Props) => ( + MIDDLE_SCREEN_RESOLUTION ? '' : title} + position="left" + anchorClassName={cx(styles.actionBtn, { [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION })} + > + <> + {width > MIDDLE_SCREEN_RESOLUTION ? ( + + {title} + + ) : ( + + )} + + +) + +export { AddItemsAction } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/edit-item-action/EditItemAction.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/edit-item-action/EditItemAction.spec.tsx new file mode 100644 index 0000000000..d50072efe9 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/edit-item-action/EditItemAction.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, EditItemAction } from './EditItemAction' + +const mockedProps = mock() + +describe('EditItemAction', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/edit-item-action/EditItemAction.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/edit-item-action/EditItemAction.tsx new file mode 100644 index 0000000000..eb0a0b28a3 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/edit-item-action/EditItemAction.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui' +import { Nullable } from 'uiSrc/utils' + +import styles from '../styles.module.scss' + +export interface Props { + title: string + isEditable: boolean + tooltipContent: Nullable + onEditItem: () => void +} + +const EditItemAction = ({ title, isEditable, tooltipContent, onEditItem }: Props) => ( +
+ + + +
+) + +export { EditItemAction } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/index.ts new file mode 100644 index 0000000000..f32ce84116 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/index.ts @@ -0,0 +1,4 @@ +export { AddItemsAction } from './add-items-action/AddItemsAction' +export { RemoveItemsAction } from './remove-items-action/RemoveItemsAction' +export { EditItemAction } from './edit-item-action/EditItemAction' +export { StreamItemsAction } from './stream-items-action/StreamItemsAction' diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/remove-items-action/RemoveItemsAction.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/remove-items-action/RemoveItemsAction.spec.tsx new file mode 100644 index 0000000000..5471e35a48 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/remove-items-action/RemoveItemsAction.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, RemoveItemsAction } from './RemoveItemsAction' + +const mockedProps = mock() + +describe('RemoveItemsAction', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/remove-items-action/RemoveItemsAction.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/remove-items-action/RemoveItemsAction.tsx new file mode 100644 index 0000000000..7f4e57f554 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/remove-items-action/RemoveItemsAction.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui' + +import styles from '../styles.module.scss' + +export interface Props { + title: string + openRemoveItemPanel: () => void +} + +const RemoveItemsAction = ({ title, openRemoveItemPanel }: Props) => ( + + + +) + +export { RemoveItemsAction } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/stream-items-action/StreamItemsAction.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/stream-items-action/StreamItemsAction.spec.tsx new file mode 100644 index 0000000000..01e1f4cbe4 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/stream-items-action/StreamItemsAction.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, StreamItemsAction } from './StreamItemsAction' + +const mockedProps = mock() + +describe('StreamItemsAction', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/stream-items-action/StreamItemsAction.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/stream-items-action/StreamItemsAction.tsx new file mode 100644 index 0000000000..070100234b --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/stream-items-action/StreamItemsAction.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import { EuiButton, EuiButtonIcon, EuiToolTip } from '@elastic/eui' +import cx from 'classnames' + +import { MIDDLE_SCREEN_RESOLUTION } from 'uiSrc/constants' +import styles from '../styles.module.scss' + +export interface Props { + width: number + title: string + openAddItemPanel: () => void +} + +const StreamItemsAction = ({ width, title, openAddItemPanel }: Props) => ( + MIDDLE_SCREEN_RESOLUTION ? '' : title} + position="left" + anchorClassName={cx(styles.actionBtn, { [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION })} + > + <> + {width > MIDDLE_SCREEN_RESOLUTION ? ( + + {title} + + ) : ( + + )} + + +) + +export { StreamItemsAction } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/styles.module.scss new file mode 100644 index 0000000000..52eb240997 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/styles.module.scss @@ -0,0 +1,12 @@ +.actionBtn { + margin-right: 12px; + position: relative; + z-index: 2; + + &.withText { + color: var(--euiTextSubduedColor) !important; + :global(.euiButton__text) { + font: normal normal normal 12px/18px Graphik !important; + } + } +} diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/ListDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/ListDetails.spec.tsx new file mode 100644 index 0000000000..7158faf4a0 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/ListDetails.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, ListDetails } from './ListDetails' + +const mockedProps = mock() + +describe('ListDetails', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/ListDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/ListDetails.tsx new file mode 100644 index 0000000000..3fc30fd283 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/ListDetails.tsx @@ -0,0 +1,90 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' + +import { + selectedKeySelector, +} from 'uiSrc/slices/browser/keys' +import { KeyTypes } from 'uiSrc/constants' + +import { KeyDetailsHeader, KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules' +import { ListDetailsTable } from './list-details-table' + +import { RemoveListElements } from './remove-list-elements' + +import AddListElements from './add-list-elements/AddListElements' +import { AddItemsAction, RemoveItemsAction } from '../key-details-actions' +import styles from './styles.module.scss' + +export interface Props extends KeyDetailsHeaderProps { + onRemoveKey: () => void + onOpenAddItemPanel: () => void + onCloseAddItemPanel: () => void +} + +const ListDetails = (props: Props) => { + const keyType = KeyTypes.List + const { onRemoveKey, onOpenAddItemPanel, onCloseAddItemPanel } = props + const { loading } = useSelector(selectedKeySelector) + + const [isRemoveItemPanelOpen, setIsRemoveItemPanelOpen] = useState(false) + const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState(false) + + const openAddItemPanel = () => { + setIsRemoveItemPanelOpen(false) + setIsAddItemPanelOpen(true) + onOpenAddItemPanel() + } + + const closeAddItemPanel = () => { + setIsAddItemPanelOpen(false) + onCloseAddItemPanel() + } + + const closeRemoveItemPanel = () => { + setIsRemoveItemPanelOpen(false) + } + + const openRemoveItemPanel = () => { + setIsAddItemPanelOpen(false) + setIsRemoveItemPanelOpen(true) + } + + const Actions = ({ width }: { width: number }) => ( + <> + + + + ) + + return ( +
+ +
+ {!loading && ( +
+ +
+ )} + {isAddItemPanelOpen && ( +
+ +
+ )} + {isRemoveItemPanelOpen && ( +
+ +
+ )} +
+
+ + ) +} + +export { ListDetails } diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-list-elements/AddListElements.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/add-list-elements/AddListElements.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-list-elements/AddListElements.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/add-list-elements/AddListElements.spec.tsx index fe9f060308..564750e0d4 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-list-elements/AddListElements.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/add-list-elements/AddListElements.spec.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' import { instance, mock } from 'ts-mockito' +import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' import AddListElements, { HEAD_DESTINATION, Props } from './AddListElements' const mockedProps = mock() diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-list-elements/AddListElements.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/add-list-elements/AddListElements.tsx similarity index 96% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-list-elements/AddListElements.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/add-list-elements/AddListElements.tsx index f9641fa108..9adfa7a564 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-list-elements/AddListElements.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/add-list-elements/AddListElements.tsx @@ -19,10 +19,9 @@ import { insertListElementsAction } from 'uiSrc/slices/browser/list' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { KeyTypes } from 'uiSrc/constants' import { stringToBuffer } from 'uiSrc/utils' +import { AddListFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' import { PushElementToListDto } from 'apiSrc/modules/browser/dto' -import { AddListFormConfig as config } from '../../add-key/constants/fields-config' - import styles from '../styles.module.scss' export interface Props { @@ -97,6 +96,7 @@ const AddListElements = (props: Props) => { color="transparent" hasShadow={false} borderRadius="none" + data-test-subj="add-list-field-panel" className={cx(styles.content, 'eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} > diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/index.ts new file mode 100644 index 0000000000..b0163cac5f --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/index.ts @@ -0,0 +1 @@ +export { ListDetails } from './ListDetails' diff --git a/redisinsight/ui/src/pages/browser/components/list-details/ListDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/ListDetailsTable.spec.tsx similarity index 87% rename from redisinsight/ui/src/pages/browser/components/list-details/ListDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/ListDetailsTable.spec.tsx index a7ac645569..fdd4b0438c 100644 --- a/redisinsight/ui/src/pages/browser/components/list-details/ListDetails.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/ListDetailsTable.spec.tsx @@ -6,7 +6,7 @@ import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { anyToBuffer } from 'uiSrc/utils' import { act, fireEvent, render, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils' import { GZIP_COMPRESSED_VALUE_1, DECOMPRESSED_VALUE_STR_1 } from 'uiSrc/utils/tests/decompressors' -import ListDetails, { Props } from './ListDetails' +import { ListDetailsTable, Props } from './ListDetailsTable' const mockedProps = mock() @@ -43,13 +43,13 @@ jest.mock('uiSrc/slices/instances/instances', () => ({ }), })) -describe('ListDetails', () => { +describe('ListDetailsTable', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) it('should render rows properly', () => { - const { container } = render() + const { container } = render() const rows = container.querySelectorAll( '.ReactVirtualized__Table__row[role="row"]' ) @@ -57,19 +57,19 @@ describe('ListDetails', () => { }) it('should render search input', () => { - render() + render() expect(screen.getByTestId('search')).toBeTruthy() }) it('should call search', () => { - render() + render() const searchInput = screen.getByTestId('search') fireEvent.change(searchInput, { target: { value: '111' } }) expect(searchInput).toHaveValue('111') }) it('should render editor after click edit button', async () => { - render() + render() await act(() => { fireEvent.click(screen.getAllByTestId(/edit-list-button/)[0]) }) @@ -77,7 +77,7 @@ describe('ListDetails', () => { }) it('should render resize trigger for index column', () => { - render() + render() expect(screen.getByTestId('resize-trigger-index')).toBeInTheDocument() }) @@ -93,7 +93,7 @@ describe('ListDetails', () => { }) listDataSelector.mockImplementation(listDataSelectorMock) - const { queryByTestId } = render() + const { queryByTestId } = render() const elementEl = queryByTestId(/list-element-value-/) expect(elementEl).toHaveTextContent(DECOMPRESSED_VALUE_STR_1) @@ -114,7 +114,7 @@ describe('ListDetails', () => { compressor: KeyValueCompressor.GZIP, })) - const { queryByTestId } = render() + const { queryByTestId } = render() const editBtn = queryByTestId(/edit-list-button-/) fireEvent.click(editBtn) diff --git a/redisinsight/ui/src/pages/browser/components/list-details/ListDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/ListDetailsTable.tsx similarity index 99% rename from redisinsight/ui/src/pages/browser/components/list-details/ListDetails.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/ListDetailsTable.tsx index 072e091f36..948a8f094e 100644 --- a/redisinsight/ui/src/pages/browser/components/list-details/ListDetails.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/ListDetailsTable.tsx @@ -79,7 +79,7 @@ export interface Props { isFooterOpen: boolean } -const ListDetails = (props: Props) => { +const ListDetailsTable = (props: Props) => { const { isFooterOpen } = props const { loading } = useSelector(listSelector) const { loading: updateLoading } = useSelector(updateListValueStateSelector) @@ -413,6 +413,7 @@ const ListDetails = (props: Props) => { return (
{ ) } -export default ListDetails +export { ListDetailsTable } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/index.ts new file mode 100644 index 0000000000..de061cae84 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/index.ts @@ -0,0 +1 @@ +export { ListDetailsTable } from './ListDetailsTable' diff --git a/redisinsight/ui/src/pages/browser/components/list-details/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/list-details/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/key-details-remove-items/remove-list-elements/RemoveListElements.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/RemoveListElements.spec.tsx similarity index 93% rename from redisinsight/ui/src/pages/browser/components/key-details-remove-items/remove-list-elements/RemoveListElements.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/RemoveListElements.spec.tsx index 35137751dd..ac78e7af6a 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-remove-items/remove-list-elements/RemoveListElements.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/RemoveListElements.spec.tsx @@ -4,8 +4,8 @@ import { instance, mock } from 'ts-mockito' import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' import { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances' -import RemoveListElements, { Props } from './RemoveListElements' -import { HEAD_DESTINATION } from '../../key-details-add-items/add-list-elements/AddListElements' +import { Props, RemoveListElements } from './RemoveListElements' +import { HEAD_DESTINATION } from '../add-list-elements/AddListElements' const COUNT_INPUT = 'count-input' diff --git a/redisinsight/ui/src/pages/browser/components/key-details-remove-items/remove-list-elements/RemoveListElements.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/RemoveListElements.tsx similarity index 97% rename from redisinsight/ui/src/pages/browser/components/key-details-remove-items/remove-list-elements/RemoveListElements.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/RemoveListElements.tsx index b882b223a4..7b9b71a289 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-remove-items/remove-list-elements/RemoveListElements.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/RemoveListElements.tsx @@ -28,16 +28,16 @@ import { selectedKeyDataSelector, keysSelector } from 'uiSrc/slices/browser/keys import { deleteListElementsAction } from 'uiSrc/slices/browser/list' import { connectedInstanceOverviewSelector, connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { AddListFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' import { DeleteListElementsDto } from 'apiSrc/modules/browser/dto' -import { AddListFormConfig as config } from '../../add-key/constants/fields-config' import { TAIL_DESTINATION, HEAD_DESTINATION, ListElementDestination, -} from '../../key-details-add-items/add-list-elements/AddListElements' +} from '../add-list-elements/AddListElements' -import styles from '../styles.module.scss' +import styles from './styles.module.scss' export interface Props { onCancel: () => void @@ -294,4 +294,4 @@ const RemoveListElements = (props: Props) => { ) } -export default RemoveListElements +export { RemoveListElements } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/index.ts new file mode 100644 index 0000000000..86e8b36cd7 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/index.ts @@ -0,0 +1 @@ +export { RemoveListElements } from './RemoveListElements' diff --git a/redisinsight/ui/src/pages/browser/components/key-details-remove-items/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/key-details-remove-items/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/styles.module.scss new file mode 100644 index 0000000000..0838dd17dc --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/styles.module.scss @@ -0,0 +1,4 @@ +.contentActive { + border-color: var(--euiColorPrimary) !important; + border-bottom-width: 1px !important; +} \ No newline at end of file diff --git a/redisinsight/ui/src/pages/browser/components/modules-type-details/ModulesTypeDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/modules-type-details/ModulesTypeDetails.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/modules-type-details/ModulesTypeDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/modules-type-details/ModulesTypeDetails.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/modules-type-details/ModulesTypeDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/modules-type-details/ModulesTypeDetails.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/modules-type-details/ModulesTypeDetails.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/modules-type-details/ModulesTypeDetails.tsx diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.spec.tsx new file mode 100644 index 0000000000..ba91fc110e --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, NoKeySelected } from './NoKeySelected' + +const mockedProps = mock() + +describe('NoKeySelected', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.tsx new file mode 100644 index 0000000000..e7bf62e510 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { EuiButtonIcon, EuiText, EuiToolTip } from '@elastic/eui' +import { useDispatch } from 'react-redux' +import ExploreGuides from 'uiSrc/components/explore-guides' +import { Nullable } from 'uiSrc/utils' + +import { toggleBrowserFullScreen } from 'uiSrc/slices/browser/keys' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import styles from './styles.module.scss' + +export interface Props { + keyProp: Nullable + totalKeys: number + keysLastRefreshTime: Nullable + onClosePanel: () => void + error?: string +} + +export const NoKeySelected = (props: Props) => { + const { + keyProp, + totalKeys, + onClosePanel, + error, + keysLastRefreshTime, + } = props + + const dispatch = useDispatch() + + const handleClosePanel = () => { + dispatch(toggleBrowserFullScreen(true)) + keyProp && onClosePanel() + } + + const NoKeysSelectedMessage = () => ( + <> + {totalKeys > 0 ? ( + + Select the key from the list on the left to see the details of the key. + + ) : ()} + + ) + + return ( + <> + + + + +
+ + {error ? ( +

+ {error} +

+ ) : (!!keysLastRefreshTime && )} +
+
+ + ) +} diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/index.ts new file mode 100644 index 0000000000..1980db48fc --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/index.ts @@ -0,0 +1 @@ +export { NoKeySelected } from './NoKeySelected' \ No newline at end of file diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/styles.module.scss new file mode 100644 index 0000000000..21be3c5580 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/styles.module.scss @@ -0,0 +1,21 @@ +.closeRightPanel { + position: absolute; + top: 22px; + right: 18px; + + .closeBtn { + :global(svg) { + width: 20px; + height: 20px; + } + } +} + +.placeholder { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + padding: 12px; + width: 100%; +} diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/JSONArray/JSONArray.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONArray/JSONArray.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/JSONArray/JSONArray.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONArray/JSONArray.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/JSONArray/JSONArray.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONArray/JSONArray.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/JSONArray/JSONArray.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONArray/JSONArray.tsx diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/JSONInterfaces.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONInterfaces.ts similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/JSONInterfaces.ts rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONInterfaces.ts diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/JSONObject/JSONObject.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONObject/JSONObject.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/JSONObject/JSONObject.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONObject/JSONObject.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/JSONObject/JSONObject.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONObject/JSONObject.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/JSONObject/JSONObject.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONObject/JSONObject.tsx diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/JSONScalar/JSONScalar.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONScalar/JSONScalar.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/JSONScalar/JSONScalar.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONScalar/JSONScalar.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/JSONScalar/JSONScalar.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONScalar/JSONScalar.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/JSONScalar/JSONScalar.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONScalar/JSONScalar.tsx diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/JSONUtils/JSONUtils.spec.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONUtils/JSONUtils.spec.ts similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/JSONUtils/JSONUtils.spec.ts rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONUtils/JSONUtils.spec.ts diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/JSONUtils/JSONUtils.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONUtils/JSONUtils.ts similarity index 88% rename from redisinsight/ui/src/pages/browser/components/rejson-details/JSONUtils/JSONUtils.ts rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONUtils/JSONUtils.ts index 6b3d9acda1..9363afa3be 100644 --- a/redisinsight/ui/src/pages/browser/components/rejson-details/JSONUtils/JSONUtils.ts +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/JSONUtils/JSONUtils.ts @@ -1,5 +1,5 @@ -import styles from 'uiSrc/pages/browser/components/rejson-details/styles.module.scss' import { JSONScalarValue } from '../JSONInterfaces' +import styles from '../styles.module.scss' enum ClassNames { string = 'jsonString', diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/RejsonDetails/RejsonDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetails/RejsonDetails.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/RejsonDetails/RejsonDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetails/RejsonDetails.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/RejsonDetails/RejsonDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetails/RejsonDetails.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/RejsonDetails/RejsonDetails.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetails/RejsonDetails.tsx diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetailsWrapper.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetailsWrapper.spec.tsx new file mode 100644 index 0000000000..9b87018a71 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetailsWrapper.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { RejsonDetailsWrapper, Props } from './RejsonDetailsWrapper' + +const mockedProps = mock() + +describe('ReJSONDetailsWrapper', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/RejsonDetailsWrapper.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetailsWrapper.tsx similarity index 51% rename from redisinsight/ui/src/pages/browser/components/rejson-details/RejsonDetailsWrapper.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetailsWrapper.tsx index 6ca3a2d8ec..da31e54606 100644 --- a/redisinsight/ui/src/pages/browser/components/rejson-details/RejsonDetailsWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetailsWrapper.tsx @@ -6,12 +6,17 @@ import { rejsonDataSelector, rejsonSelector } from 'uiSrc/slices/browser/rejson' import { selectedKeyDataSelector, keysSelector } from 'uiSrc/slices/browser/keys' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { sendEventTelemetry, TelemetryEvent, getBasedOnViewTypeEvent } from 'uiSrc/telemetry' +import { KeyDetailsHeader, KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules' +import { KeyTypes } from 'uiSrc/constants' import RejsonDetails from './RejsonDetails/RejsonDetails' import styles from './styles.module.scss' -const RejsonDetailsWrapper = () => { +export interface Props extends KeyDetailsHeaderProps {} + +const RejsonDetailsWrapper = (props: Props) => { + const keyType = KeyTypes.ReJSON const { loading } = useSelector(rejsonSelector) const { data, downloaded, type } = useSelector(rejsonDataSelector) const { name: selectedKey = '' } = useSelector(selectedKeyDataSelector) || {} @@ -67,38 +72,54 @@ const RejsonDetailsWrapper = () => { const reportJSONPropertyAdded = () => {} return ( -
- {loading && ( - - )} - {!(loading && data === undefined) && ( - {}} - handleOpenExpiryDialog={() => {}} - keyProperty={{}} - /> - )} +
+ +
+ {!loading && ( +
+
+ {loading && ( + + )} + {!(loading && data === undefined) && ( + {}} + handleOpenExpiryDialog={() => {}} + keyProperty={{}} + /> + )} +
+
+ )} +
) } -export default RejsonDetailsWrapper +export { RejsonDetailsWrapper } diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/constants.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/constants.ts similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/constants.ts rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/constants.ts diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/index.ts new file mode 100644 index 0000000000..fa6bc0e31c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/index.ts @@ -0,0 +1 @@ +export { RejsonDetailsWrapper } from './RejsonDetailsWrapper' diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/rejson-details/styles.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/styles.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/rejson-details/styles.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/styles.scss diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/SetDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/SetDetails.spec.tsx new file mode 100644 index 0000000000..fd89ca6739 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/SetDetails.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, SetDetails } from './SetDetails' + +const mockedProps = mock() + +describe('SetDetails', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/SetDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/SetDetails.tsx new file mode 100644 index 0000000000..8ef388c9c5 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/SetDetails.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' + +import { + selectedKeySelector, +} from 'uiSrc/slices/browser/keys' +import { KeyTypes } from 'uiSrc/constants' + +import { KeyDetailsHeader, KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules' +import { SetDetailsTable } from './set-details-table' +import { AddSetMembers } from './add-set-members' +import { AddItemsAction } from '../key-details-actions' + +export interface Props extends KeyDetailsHeaderProps { + onRemoveKey: () => void + onOpenAddItemPanel: () => void + onCloseAddItemPanel: () => void +} + +const SetDetails = (props: Props) => { + const keyType = KeyTypes.Set + const { onRemoveKey, onOpenAddItemPanel, onCloseAddItemPanel } = props + + const { loading } = useSelector(selectedKeySelector) + + const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState(false) + + const openAddItemPanel = () => { + setIsAddItemPanelOpen(true) + onOpenAddItemPanel() + } + + const closeAddItemPanel = () => { + setIsAddItemPanelOpen(false) + onCloseAddItemPanel() + } + + const Actions = ({ width }: { width: number }) => ( + + ) + + return ( +
+ +
+ {!loading && ( +
+ +
+ )} + {isAddItemPanelOpen && ( +
+ +
+ )} +
+
+ + ) +} + +export { SetDetails } diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-set-members/AddSetMembers.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.spec.tsx similarity index 97% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-set-members/AddSetMembers.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.spec.tsx index 53202495b6..49441d1f91 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-set-members/AddSetMembers.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.spec.tsx @@ -1,7 +1,7 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' -import AddSetMembers, { Props } from './AddSetMembers' +import { AddSetMembers, Props } from './AddSetMembers' const MEMBER_NAME = 'member-name' const ADD_NEW_ITEM = 'add-new-item' diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-set-members/AddSetMembers.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.tsx similarity index 94% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-set-members/AddSetMembers.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.tsx index 6a0944ef8d..1fa3141644 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-set-members/AddSetMembers.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.tsx @@ -18,10 +18,8 @@ import { KeyTypes } from 'uiSrc/constants' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { stringToBuffer } from 'uiSrc/utils' -import AddItemsActions from '../../add-items-actions/AddItemsActions' -import { AddZsetFormConfig as config } from '../../add-key/constants/fields-config' - -import styles from '../styles.module.scss' +import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' +import { AddZsetFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' export interface Props { onCancel: (isCancelled?: boolean) => void; @@ -124,7 +122,8 @@ const AddSetMembers = (props: Props) => { color="transparent" hasShadow={false} borderRadius="none" - className={cx(styles.content, 'eui-yScroll', 'flexItemNoFullWidth')} + data-test-subj="add-set-field-panel" + className={cx('eui-yScroll', 'flexItemNoFullWidth')} > {members.map((item, index) => ( @@ -159,7 +158,6 @@ const AddSetMembers = (props: Props) => { clearIsDisabled={isClearDisabled(item)} clearItemValues={clearMemberValues} loading={loading} - anchorClassName={styles.refreshKeyTooltip} /> @@ -196,4 +194,4 @@ const AddSetMembers = (props: Props) => { ) } -export default AddSetMembers +export { AddSetMembers } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/index.ts new file mode 100644 index 0000000000..0d82389552 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/index.ts @@ -0,0 +1 @@ +export { AddSetMembers } from './AddSetMembers' diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/index.ts new file mode 100644 index 0000000000..b2d160c4a7 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/index.ts @@ -0,0 +1 @@ +export { SetDetails } from './SetDetails' diff --git a/redisinsight/ui/src/pages/browser/components/set-details/SetDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/SetDetailsTable.spec.tsx similarity index 81% rename from redisinsight/ui/src/pages/browser/components/set-details/SetDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/SetDetailsTable.spec.tsx index cf122e8492..e49bc85818 100644 --- a/redisinsight/ui/src/pages/browser/components/set-details/SetDetails.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/SetDetailsTable.spec.tsx @@ -4,7 +4,7 @@ import { setDataSelector } from 'uiSrc/slices/browser/set' import { anyToBuffer } from 'uiSrc/utils' import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' import { GZIP_COMPRESSED_VALUE_1, DECOMPRESSED_VALUE_STR_1 } from 'uiSrc/utils/tests/decompressors' -import SetDetails, { Props } from './SetDetails' +import { SetDetailsTable, Props } from './SetDetailsTable' const members = [ { type: 'Buffer', data: [49] }, @@ -28,24 +28,24 @@ jest.mock('uiSrc/slices/browser/set', () => { }) }) -describe('SetDetails', () => { +describe('SetDetailsTable', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) it('should render rows properly', () => { - const { container } = render() + const { container } = render() const rows = container.querySelectorAll('.ReactVirtualized__Table__row[role="row"]') expect(rows).toHaveLength(members.length) }) it('should render search input', () => { - render() + render() expect(screen.getByTestId('search')).toBeTruthy() }) it('should call search', () => { - render() + render() const searchInput = screen.getByTestId('search') fireEvent.change( searchInput, @@ -55,7 +55,7 @@ describe('SetDetails', () => { }) it('should render delete popup after click remove button', () => { - render() + render() fireEvent.click(screen.getAllByTestId(/set-remove-btn/)[0]) expect(screen.getByTestId(/set-remove-btn-1-icon/)).toBeInTheDocument() }) @@ -72,7 +72,7 @@ describe('SetDetails', () => { }) setDataSelector.mockImplementation(setDataSelectorMock) - const { queryByTestId } = render() + const { queryByTestId } = render() const memberEl = queryByTestId(/set-member-value-/) expect(memberEl).toHaveTextContent(DECOMPRESSED_VALUE_STR_1) diff --git a/redisinsight/ui/src/pages/browser/components/set-details/SetDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/SetDetailsTable.tsx similarity index 98% rename from redisinsight/ui/src/pages/browser/components/set-details/SetDetails.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/SetDetailsTable.tsx index a1bb96a231..e4aebab83a 100644 --- a/redisinsight/ui/src/pages/browser/components/set-details/SetDetails.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/SetDetailsTable.tsx @@ -54,7 +54,7 @@ export interface Props { onRemoveKey: () => void } -const SetDetails = (props: Props) => { +const SetDetailsTable = (props: Props) => { const { isFooterOpen, onRemoveKey } = props const { loading } = useSelector(setSelector) @@ -262,6 +262,7 @@ const SetDetails = (props: Props) => { return (
{ ) } -export default SetDetails +export { SetDetailsTable } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/index.ts new file mode 100644 index 0000000000..8c6311a3f3 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/index.ts @@ -0,0 +1 @@ +export { SetDetailsTable } from './SetDetailsTable' diff --git a/redisinsight/ui/src/pages/browser/components/set-details/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/set-details/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/StreamDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/StreamDetails.spec.tsx new file mode 100644 index 0000000000..25de1bf3d4 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/StreamDetails.spec.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { fireEvent, render } from 'uiSrc/utils/test-utils' +import { streamSelector } from 'uiSrc/slices/browser/stream' +import { StreamViewType } from 'uiSrc/slices/interfaces/stream' +import { Props, StreamDetails } from './StreamDetails' + +const mockedProps = mock() + +jest.mock('uiSrc/slices/browser/stream', () => ({ + ...jest.requireActual('uiSrc/slices/browser/stream'), + streamSelector: jest.fn().mockReturnValue({ + viewType: 'Data', + }), +})) + +describe('StreamDetails', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('"add-key-value-items-btn" should render', () => { + const { queryByTestId } = render() + expect(queryByTestId('add-key-value-items-btn')).toBeInTheDocument() + }) + + it('"add-stream-field-panel" should render', () => { + const { container, queryByTestId } = render( + {}} + /> + ) + + fireEvent.click(queryByTestId('add-key-value-items-btn')!) + expect(container.querySelector('[data-test-subj="add-stream-field-panel"]')).toBeInTheDocument() + expect(container.querySelector('[data-test-subj="add-stream-groups-field-panel"]')).not.toBeInTheDocument() + }) + it('"add-stream-groups-field-panel" should render', () => { + const streamSelectorMock = jest.fn().mockReturnValue({ + viewType: StreamViewType.Groups, + }); + (streamSelector as jest.Mock).mockImplementation(streamSelectorMock) + + const { container, queryByTestId } = render( + {}} + /> + ) + + fireEvent.click(queryByTestId('add-key-value-items-btn')!) + expect(container.querySelector('[data-test-subj="add-stream-field-panel"]')).not.toBeInTheDocument() + expect(container.querySelector('[data-test-subj="add-stream-groups-field-panel"]')).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/StreamDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/StreamDetails.tsx new file mode 100644 index 0000000000..53c71be024 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/StreamDetails.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' + +import { + selectedKeySelector, +} from 'uiSrc/slices/browser/keys' +import { KeyTypes, STREAM_ADD_ACTION, STREAM_ADD_GROUP_VIEW_TYPES } from 'uiSrc/constants' + +import { KeyDetailsHeader, KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules' +import { streamSelector } from 'uiSrc/slices/browser/stream' +import { StreamViewType } from 'uiSrc/slices/interfaces/stream' +import { StreamDetailsBody } from './stream-details-body' +import AddStreamEntries from './add-stream-entity' +import AddStreamGroup from './add-stream-group' +import { StreamItemsAction } from '../key-details-actions' + +export interface Props extends KeyDetailsHeaderProps { + onRemoveKey: () => void + onOpenAddItemPanel: () => void + onCloseAddItemPanel: () => void +} + +const StreamDetails = (props: Props) => { + const keyType = KeyTypes.Stream + const { onOpenAddItemPanel, onCloseAddItemPanel } = props + + const { loading } = useSelector(selectedKeySelector) + const { viewType: streamViewType } = useSelector(streamSelector) + + const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState(false) + + const openAddItemPanel = () => { + setIsAddItemPanelOpen(true) + + if (!STREAM_ADD_GROUP_VIEW_TYPES.includes(streamViewType)) { + onOpenAddItemPanel() + } + } + + const closeAddItemPanel = (isCancelled?: boolean) => { + setIsAddItemPanelOpen(false) + if (isCancelled && isAddItemPanelOpen && !STREAM_ADD_GROUP_VIEW_TYPES.includes(streamViewType)) { + onCloseAddItemPanel() + } + } + + const Actions = ({ width }: { width: number }) => ( + + ) + + return ( +
+ +
+ {!loading && ( +
+ +
+ )} + {isAddItemPanelOpen && ( +
+ {streamViewType === StreamViewType.Data && ( + + )} + {STREAM_ADD_GROUP_VIEW_TYPES.includes(streamViewType!) && ( + + )} +
+ )} +
+
+ ) +} + +export { StreamDetails } diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-entity/AddStreamEntries.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/AddStreamEntries.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-entity/AddStreamEntries.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/AddStreamEntries.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-entity/AddStreamEntries.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/AddStreamEntries.tsx similarity index 97% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-entity/AddStreamEntries.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/AddStreamEntries.tsx index e5ce425e6a..8b87d26c52 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-entity/AddStreamEntries.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/AddStreamEntries.tsx @@ -9,11 +9,11 @@ import { addNewEntriesAction, streamDataSelector } from 'uiSrc/slices/browser/st import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { AddStreamFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' import { INITIAL_STREAM_FIELD_STATE } from 'uiSrc/pages/browser/components/add-key/AddKeyStream/AddKeyStream' -import { StreamEntryFields } from 'uiSrc/pages/browser/components/key-details-add-items' import { KeyTypes } from 'uiSrc/constants' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { AddStreamEntriesDto } from 'apiSrc/modules/browser/dto/stream.dto' +import StreamEntryFields from './StreamEntryFields/StreamEntryFields' import styles from './styles.module.scss' export interface Props { @@ -109,6 +109,7 @@ const AddStreamEntries = (props: Props) => { color="transparent" hasShadow={false} borderRadius="none" + data-test-subj="add-stream-field-panel" className={cx(styles.content, 'eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} > { removeItem={removeField} clearItemValues={clearFieldsValues} clearIsDisabled={isClearDisabled(item)} - anchorClassName={styles.refreshKeyTooltip} /> diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-entity/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/index.ts similarity index 100% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-entity/index.ts rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/index.ts diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-entity/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-entity/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-group/AddStreamGroup.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-group/AddStreamGroup.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-group/AddStreamGroup.tsx similarity index 99% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-group/AddStreamGroup.tsx index 0b17d99b78..465a49f2ca 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-stream-group/AddStreamGroup.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-group/AddStreamGroup.tsx @@ -85,6 +85,7 @@ const AddStreamGroup = (props: Props) => { color="transparent" hasShadow={false} borderRadius="none" + data-test-subj="add-stream-groups-field-panel" className={cx(styles.content, 'eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} > ({ ...jest.requireActual('uiSrc/slices/browser/stream'), @@ -59,13 +59,13 @@ const mockedRangeData = { end: '1675751507406', } -describe('StreamDetailsWrapper', () => { +describe('StreamDetailsBody', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) it('should render Stream Data container', () => { - render() + render() expect(screen.getByTestId('stream-entries-container')).toBeInTheDocument() }) @@ -79,7 +79,7 @@ describe('StreamDetailsWrapper', () => { ...mockedRangeData, })) - render() + render() expect(screen.getByTestId('range-bar')).toBeInTheDocument() }) @@ -105,7 +105,7 @@ describe('StreamDetailsWrapper', () => { ...mockedRangeData, })) - const { queryByTestId } = render() + const { queryByTestId } = render() expect(queryByTestId('range-bar')).not.toBeInTheDocument() }) @@ -130,7 +130,7 @@ describe('StreamDetailsWrapper', () => { ], })) - const { queryAllByTestId } = render() + const { queryAllByTestId } = render() const fieldNameEl = queryAllByTestId(/stream-field-name-/)?.[0] const entryFieldEl = queryAllByTestId(/stream-entry-field-/)?.[0] diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/StreamDetailsBody.tsx similarity index 93% rename from redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/StreamDetailsBody.tsx index 827f5f1f4d..d9646806d7 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/StreamDetailsWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/StreamDetailsBody.tsx @@ -25,12 +25,12 @@ import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import RangeFilter from 'uiSrc/components/range-filter' import { GetStreamEntriesResponse } from 'apiSrc/modules/browser/dto/stream.dto' -import ConsumersViewWrapper from './consumers-view' -import GroupsViewWrapper from './groups-view' -import MessagesViewWrapper from './messages-view' -import StreamDataViewWrapper from './stream-data-view' -import StreamTabs from './stream-tabs' -import { MAX_FORMAT_LENGTH_STREAM_TIMESTAMP } from './constants' +import ConsumersViewWrapper from '../consumers-view' +import GroupsViewWrapper from '../groups-view' +import MessagesViewWrapper from '../messages-view' +import StreamDataViewWrapper from '../stream-data-view' +import StreamTabs from '../stream-tabs' +import { MAX_FORMAT_LENGTH_STREAM_TIMESTAMP } from '../constants' import styles from './styles.module.scss' @@ -38,7 +38,7 @@ export interface Props { isFooterOpen: boolean } -const StreamDetailsWrapper = (props: Props) => { +const StreamDetailsBody = (props: Props) => { const { viewType, loading, sortOrder: entryColumnSortOrder } = useSelector(streamSelector) const { loading: loadingGroups } = useSelector(streamGroupsSelector) const { start, end } = useSelector(streamRangeSelector) @@ -183,7 +183,10 @@ const StreamDetailsWrapper = (props: Props) => { ) return ( -
+
{(loading || loadingGroups) && ( { ) } -export default StreamDetailsWrapper +export { StreamDetailsBody } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/index.ts new file mode 100644 index 0000000000..08998551aa --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/index.ts @@ -0,0 +1 @@ +export { StreamDetailsBody } from './StreamDetailsBody' diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/stream-details/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/StreamTabs.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/StreamTabs.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/StreamTabs.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/StreamTabs.tsx diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/index.ts similarity index 100% rename from redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/index.ts rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/index.ts diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/StringDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/StringDetails.spec.tsx new file mode 100644 index 0000000000..2b50cd7536 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/StringDetails.spec.tsx @@ -0,0 +1,90 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render, screen } from 'uiSrc/utils/test-utils' +import { stringDataSelector, stringSelector } from 'uiSrc/slices/browser/string' +import { Props, StringDetails } from './StringDetails' + +const mockedProps = mock() +const EDIT_VALUE_BTN_TEST_ID = 'edit-key-value-btn' + +jest.mock('uiSrc/slices/browser/string', () => ({ + ...jest.requireActual('uiSrc/slices/browser/string'), + stringDataSelector: jest.fn().mockReturnValue({ + value: { + type: 'Buffer', + data: [49, 50, 51, 52], + } + }), + stringSelector: jest.fn().mockReturnValue({ + isCompressed: false + }) +})) + +jest.mock('uiSrc/slices/browser/keys', () => ({ + ...jest.requireActual('uiSrc/slices/browser/keys'), + selectedKeyDataSelector: jest.fn().mockReturnValue({ + name: { + type: 'Buffer', + data: [116, 101, 115, 116] + }, + nameString: 'test', + length: 4 + }), +})) + +describe('StringDetails', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should be able to change value (long string fully load)', () => { + render( + + ) + + const editValueBtn = screen.getByTestId(`${EDIT_VALUE_BTN_TEST_ID}`) + expect(editValueBtn).toHaveProperty('disabled', false) + }) + + it('should not be able to change value (long string not fully load)', () => { + const stringDataSelectorMock = jest.fn().mockReturnValue({ + value: { + type: 'Buffer', + data: [49, 50, 51], + } + }) + stringDataSelector.mockImplementation(stringDataSelectorMock) + + render( + + ) + + const editValueBtn = screen.getByTestId(`${EDIT_VALUE_BTN_TEST_ID}`) + expect(editValueBtn).toHaveProperty('disabled', true) + }) + + it('should not be able to change value (compressed)', () => { + const stringSelectorMock = jest.fn().mockReturnValue({ + isCompressed: true + }) + stringSelector.mockImplementation(stringSelectorMock) + + render( + + ) + + const editValueBtn = screen.getByTestId(`${EDIT_VALUE_BTN_TEST_ID}`) + expect(editValueBtn).toHaveProperty('disabled', true) + }) + + it('"edit-key-value-btn" should render', () => { + const { queryByTestId } = render() + expect(queryByTestId('edit-key-value-btn')).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/StringDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/StringDetails.tsx new file mode 100644 index 0000000000..279614065c --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/StringDetails.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { + initialKeyInfo, + refreshKey, + selectedKeyDataSelector, + selectedKeySelector, +} from 'uiSrc/slices/browser/keys' +import { + KeyTypes, + ModulesKeyTypes, + TEXT_DISABLED_COMPRESSED_VALUE, + TEXT_DISABLED_FORMATTER_EDITING, + TEXT_DISABLED_STRING_EDITING, +} from 'uiSrc/constants' + +import { KeyDetailsHeader, KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { IFetchKeyArgs } from 'uiSrc/constants/prop-types/keys' +import { resetStringValue, stringDataSelector, stringSelector } from 'uiSrc/slices/browser/string' +import { isFormatEditable, isFullStringLoaded } from 'uiSrc/utils' +import { StringDetailsTable } from './string-details-table' +import { EditItemAction } from '../key-details-actions' + +export interface Props extends KeyDetailsHeaderProps {} + +const StringDetails = (props: Props) => { + const { onRemoveKey } = props + const keyType = KeyTypes.String + + const { loading, viewFormat: viewFormatProp } = useSelector(selectedKeySelector) + const { length } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo + const { value: keyValue } = useSelector(stringDataSelector) + const { isCompressed: isStringCompressed } = useSelector(stringSelector) + + const isEditable = !isStringCompressed && isFormatEditable(viewFormatProp) + const isStringEditable = isFullStringLoaded(keyValue?.data?.length, length) + const noEditableText = isStringCompressed ? TEXT_DISABLED_COMPRESSED_VALUE : TEXT_DISABLED_FORMATTER_EDITING + const editToolTip = !isEditable ? noEditableText : (!isStringEditable ? TEXT_DISABLED_STRING_EDITING : null) + + const [editItem, setEditItem] = useState(false) + + const dispatch = useDispatch() + + const handleRefreshKey = (key: RedisResponseBuffer, type: KeyTypes | ModulesKeyTypes, args: IFetchKeyArgs) => { + dispatch(refreshKey(key, type, args)) + } + + const handleRemoveKey = () => { + dispatch(resetStringValue()) + onRemoveKey() + } + + const Actions = () => ( + setEditItem(!editItem)} + /> + ) + + return ( +
+ +
+ {!loading && ( +
+ setEditItem(isEdit)} + onRefresh={handleRefreshKey} + /> +
+ )} +
+
+ ) +} + +export { StringDetails } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/index.ts new file mode 100644 index 0000000000..85694dd7fa --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/index.ts @@ -0,0 +1 @@ +export { StringDetails } from './StringDetails' diff --git a/redisinsight/ui/src/pages/browser/components/string-details/StringDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-table/StringDetailsTable.spec.tsx similarity index 85% rename from redisinsight/ui/src/pages/browser/components/string-details/StringDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-table/StringDetailsTable.spec.tsx index 1d525b2a8b..8771a36ad6 100644 --- a/redisinsight/ui/src/pages/browser/components/string-details/StringDetails.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-table/StringDetailsTable.spec.tsx @@ -16,7 +16,7 @@ import { } from 'uiSrc/utils/tests/decompressors' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { downloadFile } from 'uiSrc/utils/dom/downloadFile' -import StringDetails, { Props } from './StringDetails' +import { StringDetailsTable, Props } from './StringDetailsTable' const STRING_VALUE = 'string-value' const STRING_VALUE_SPACE = 'string value' @@ -70,11 +70,11 @@ jest.mock('react-redux', () => ({ useDispatch: () => jest.fn().mockReturnValue(() => jest.fn()), })) -describe('StringDetails', () => { +describe('StringDetailsTable', () => { it('should render', () => { expect( render( - ) @@ -83,7 +83,7 @@ describe('StringDetails', () => { it('should render textarea if edit mode', () => { render( - { it('should update string value', () => { render( - { it('should stay empty string after cancel', async () => { render( - { it('should update value after apply', () => { render( - { stringDataSelector.mockImplementation(stringDataSelectorMock) render( - ) @@ -173,7 +173,7 @@ describe('StringDetails', () => { stringDataSelector.mockImplementation(stringDataSelectorMock) render( - @@ -190,6 +190,34 @@ describe('StringDetails', () => { }) }) + it('Should add "..." in the end of the part value', async () => { + const stringDataSelectorMock = jest.fn().mockReturnValue({ + value: partValue + }) + stringDataSelector.mockImplementation(stringDataSelectorMock) + + render( + + ) + expect(screen.getByTestId(STRING_VALUE)).toHaveTextContent(`${bufferToString(partValue)}...`) + }) + + it('Should not add "..." in the end of the full value', async () => { + const stringDataSelectorMock = jest.fn().mockReturnValue({ + value: fullValue + }) + stringDataSelector.mockImplementation(stringDataSelectorMock) + + render( + + ) + expect(screen.getByTestId(STRING_VALUE)).toHaveTextContent(bufferToString(fullValue)) + }) + it('should call fetchDownloadStringValue and sendEventTelemetry after clicking on load button and download button', async () => { const stringDataSelectorMock = jest.fn().mockReturnValue({ value: partValue @@ -197,7 +225,7 @@ describe('StringDetails', () => { stringDataSelector.mockImplementation(stringDataSelectorMock) render( - ) @@ -225,7 +253,7 @@ describe('StringDetails', () => { })) render( - { })) render( - void; } -const StringDetails = (props: Props) => { +const StringDetailsTable = (props: Props) => { const { isEditItem, setIsEdit, onRefresh } = props const { compressor = null } = useSelector(connectedInstanceSelector) @@ -72,7 +71,7 @@ const StringDetails = (props: Props) => { const { name: key, type: keyType, length } = useSelector(selectedKeyDataSelector) ?? { name: '' } const { viewFormat: viewFormatProp } = useSelector(selectedKeySelector) - const [rows, setRows] = useState(5) + const [rows, setRows] = useState(MIN_ROWS) const [value, setValue] = useState('') const [areaValue, setAreaValue] = useState('') const [viewFormat, setViewFormat] = useState(viewFormatProp) @@ -83,6 +82,7 @@ const StringDetails = (props: Props) => { const textAreaRef: Ref = useRef(null) const viewValueRef: Ref = useRef(null) + const containerRef: Ref = useRef(null) const dispatch = useDispatch() @@ -99,7 +99,7 @@ const StringDetails = (props: Props) => { const { value: formattedValue, isValid } = formattingBuffer(decompressedValue, viewFormatProp, { expanded: true }) setAreaValue(initialValueString) - setValue(formattedValue) + setValue(!isFullStringLoaded(initialValue?.data?.length, length) ? `${formattedValue}...` : formattedValue) setIsValid(isValid) setIsDisabled( !isNonUnicodeFormatter(viewFormatProp, isValid) @@ -125,16 +125,9 @@ const StringDetails = (props: Props) => { return } const calculatedRows = calculateTextareaLines(areaValue, textAreaRef.current.clientWidth, APPROXIMATE_WIDTH_OF_SIGN) - - if (calculatedRows > MAX_ROWS) { - setRows(MAX_ROWS) - return + if (calculatedRows > MIN_ROWS) { + setRows(calculatedRows) } - if (calculatedRows < MIN_ROWS) { - setRows(MIN_ROWS) - return - } - setRows(calculatedRows) }, [viewValueRef, isEditItem]) useMemo(() => { @@ -188,7 +181,7 @@ const StringDetails = (props: Props) => { return ( <> -
+
{isLoading && ( { /> )} {!isEditItem && ( - isEditable && setIsEdit(true)} - style={{ whiteSpace: 'break-spaces' }} - data-testid="string-value" + - {areaValue !== '' - ? (isValid + isEditable && setIsEdit(true)} + style={{ whiteSpace: 'break-spaces' }} + data-testid="string-value" + > + {areaValue !== '' ? value - : ( - - <>{value} - - ) - ) - : (!isLoading && (Empty))} - + : (!isLoading && (Empty))} + + )} {isEditItem && ( { disabled={loading} inputRef={textAreaRef} className={cx(styles.stringTextArea, { [styles.areaWarning]: isDisabled })} + style={{ maxHeight: containerRef.current ? containerRef.current?.clientHeight - 80 : '100%' }} data-testid="string-value" /> @@ -300,4 +291,4 @@ const StringDetails = (props: Props) => { ) } -export default StringDetails +export { StringDetailsTable } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-table/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-table/index.ts new file mode 100644 index 0000000000..f62de5cbed --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-table/index.ts @@ -0,0 +1 @@ +export { StringDetailsTable } from './StringDetailsTable' diff --git a/redisinsight/ui/src/pages/browser/components/string-details/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-table/styles.module.scss similarity index 80% rename from redisinsight/ui/src/pages/browser/components/string-details/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-table/styles.module.scss index 3ae9881baf..764fa4e7d7 100644 --- a/redisinsight/ui/src/pages/browser/components/string-details/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-table/styles.module.scss @@ -6,14 +6,10 @@ $outer-height: 220px; $outer-height-mobile: 340px; .container { - @include euiScrollBar; padding: 20px 16px 20px; - max-height: calc(100vh - #{$outer-height}); - overflow-y: auto; - overflow-x: hidden; - word-break: break-word; + overflow: hidden; color: var(--euiTextSubduedColor); flex: 1; position: relative; @@ -29,10 +25,12 @@ $outer-height-mobile: 340px; } .stringValue { - font: inherit !important; - color: inherit !important; - padding: 0 !important; - background: inherit !important; + @include euiScrollBar; + overflow-y: auto; + overflow-x: hidden; + word-break: break-word; + line-height: 1.2; + width: 100%; pre { background-color: transparent !important; @@ -41,17 +39,19 @@ $outer-height-mobile: 340px; } .stringTextArea { - max-height: calc(100vh - #{$outer-height} - 55px); - @media only screen and (max-width: 767px) { - max-height: calc(100vh - #{$outer-height-mobile} - 55px); - } - &.areaWarning { border-color: var(--euiColorWarningLight) !important; background-image: none !important; } } +.tooltipAnchor { + display: inline-flex !important; + height: auto; + max-height: 100%; + width: 100%; +} + .stringFooterBtn { &:global(.euiButton) { color: var(--euiTextSubduedColor) !important; diff --git a/redisinsight/ui/src/pages/browser/components/unsupported-type-details/UnsupportedTypeDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/unsupported-type-details/UnsupportedTypeDetails.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/unsupported-type-details/UnsupportedTypeDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/unsupported-type-details/UnsupportedTypeDetails.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/unsupported-type-details/UnsupportedTypeDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/unsupported-type-details/UnsupportedTypeDetails.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/unsupported-type-details/UnsupportedTypeDetails.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/unsupported-type-details/UnsupportedTypeDetails.tsx diff --git a/redisinsight/ui/src/pages/browser/components/unsupported-type-details/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/unsupported-type-details/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/unsupported-type-details/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/unsupported-type-details/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/ZSetDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/ZSetDetails.spec.tsx new file mode 100644 index 0000000000..3c0c3e3aa1 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/ZSetDetails.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import { Props, ZSetDetails } from './ZSetDetails' + +const mockedProps = mock() + +describe('ZSetDetails', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/ZSetDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/ZSetDetails.tsx new file mode 100644 index 0000000000..80392733fa --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/ZSetDetails.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' + +import { + selectedKeySelector, +} from 'uiSrc/slices/browser/keys' +import { KeyTypes } from 'uiSrc/constants' + +import { KeyDetailsHeader, KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules' +import { ZSetDetailsTable } from './zset-details-table' +import AddZsetMembers from './add-zset-members/AddZsetMembers' +import { AddItemsAction } from '../key-details-actions' + +export interface Props extends KeyDetailsHeaderProps { + onRemoveKey: () => void + onOpenAddItemPanel: () => void + onCloseAddItemPanel: () => void +} + +const ZSetDetails = (props: Props) => { + const keyType = KeyTypes.ZSet + const { onRemoveKey, onOpenAddItemPanel, onCloseAddItemPanel } = props + + const { loading } = useSelector(selectedKeySelector) + + const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState(false) + + const openAddItemPanel = () => { + setIsAddItemPanelOpen(true) + onOpenAddItemPanel() + } + + const closeAddItemPanel = () => { + setIsAddItemPanelOpen(false) + onCloseAddItemPanel() + } + + const Actions = ({ width }: { width: number }) => ( + + ) + + return ( +
+ +
+ {!loading && ( +
+ +
+ )} + {isAddItemPanelOpen && ( +
+ +
+ )} +
+
+ + ) +} + +export { ZSetDetails } diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-zset-members/AddZsetMembers.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-zset-members/AddZsetMembers.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.spec.tsx diff --git a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-zset-members/AddZsetMembers.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.tsx similarity index 95% rename from redisinsight/ui/src/pages/browser/components/key-details-add-items/add-zset-members/AddZsetMembers.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.tsx index 02debb3930..3be632d355 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-add-items/add-zset-members/AddZsetMembers.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.tsx @@ -21,10 +21,8 @@ import { updateZsetScoreStateSelector, } from 'uiSrc/slices/browser/zset' -import AddItemsActions from '../../add-items-actions/AddItemsActions' -import { AddZsetFormConfig as config } from '../../add-key/constants/fields-config' - -import styles from '../styles.module.scss' +import { AddZsetFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' +import AddItemsActions from 'uiSrc/pages/browser/components/add-items-actions/AddItemsActions' export interface Props { onCancel: (isCancelled?: boolean) => void; @@ -173,7 +171,8 @@ const AddZsetMembers = (props: Props) => { color="transparent" hasShadow={false} borderRadius="none" - className={cx(styles.content, 'eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} + data-test-subj="add-zset-field-panel" + className={cx('eui-yScroll', 'flexItemNoFullWidth', 'inlineFieldsNoSpace')} > {members.map((item, index) => ( @@ -228,7 +227,6 @@ const AddZsetMembers = (props: Props) => { addItemIsDisabled={members.some((item) => !item.score.length)} clearItemValues={clearMemberValues} loading={loading} - anchorClassName={styles.refreshKeyTooltip} /> diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/index.ts new file mode 100644 index 0000000000..a69b5eb401 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/index.ts @@ -0,0 +1 @@ +export { ZSetDetails } from './ZSetDetails' diff --git a/redisinsight/ui/src/pages/browser/components/zset-details/ZSetDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/ZSetDetailsTable.spec.tsx similarity index 82% rename from redisinsight/ui/src/pages/browser/components/zset-details/ZSetDetails.spec.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/ZSetDetailsTable.spec.tsx index 9cf6367abf..5ee59f5c38 100644 --- a/redisinsight/ui/src/pages/browser/components/zset-details/ZSetDetails.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/ZSetDetailsTable.spec.tsx @@ -4,7 +4,7 @@ import { zsetDataSelector } from 'uiSrc/slices/browser/zset' import { anyToBuffer } from 'uiSrc/utils' import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' import { GZIP_COMPRESSED_VALUE_1, DECOMPRESSED_VALUE_STR_1 } from 'uiSrc/utils/tests/decompressors' -import ZSetDetails, { Props } from './ZSetDetails' +import { ZSetDetailsTable, Props } from './ZSetDetailsTable' const mockedProps = mock() @@ -30,18 +30,18 @@ jest.mock('uiSrc/slices/browser/zset', () => { }) }) -describe('ZSetDetails', () => { +describe('ZSetDetailsTable', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) it('should render search input', () => { - render() + render() expect(screen.getByPlaceholderText(/search/i)).toBeTruthy() }) it('should call search', () => { - render() + render() const searchInput = screen.getByPlaceholderText(/search/i) fireEvent.change( searchInput, @@ -51,23 +51,23 @@ describe('ZSetDetails', () => { }) it('should render delete popup after click remove button', () => { - render() + render() fireEvent.click(screen.getAllByTestId(/zset-edit-button/)[0]) expect(screen.getByTestId(/zset-edit-button-1/)).toBeInTheDocument() }) it('should render disabled edit button', () => { - render() + render() expect(screen.getByTestId(/zset-edit-button-4/)).toBeDisabled() }) it('should render enabled edit button', () => { - render() + render() expect(screen.getByTestId(/zset-edit-button-3/)).not.toBeDisabled() }) it('should render editor after click edit button and able to change value', () => { - render() + render() fireEvent.click(screen.getAllByTestId(/zset-edit-button/)[0]) expect(screen.getByTestId('inline-item-editor')).toBeInTheDocument() fireEvent.change(screen.getByTestId('inline-item-editor'), { target: { value: '123' } }) @@ -75,7 +75,7 @@ describe('ZSetDetails', () => { }) it('should render resize trigger for name column', () => { - render() + render() expect(screen.getByTestId('resize-trigger-name')).toBeInTheDocument() }) @@ -91,7 +91,7 @@ describe('ZSetDetails', () => { }) zsetDataSelector.mockImplementation(zsetDataSelectorMock) - const { queryByTestId } = render() + const { queryByTestId } = render() const memberEl = queryByTestId(/zset-member-value-/) expect(memberEl).toHaveTextContent(DECOMPRESSED_VALUE_STR_1) diff --git a/redisinsight/ui/src/pages/browser/components/zset-details/ZSetDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/ZSetDetailsTable.tsx similarity index 98% rename from redisinsight/ui/src/pages/browser/components/zset-details/ZSetDetails.tsx rename to redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/ZSetDetailsTable.tsx index 790de07e6f..b969fba9cc 100644 --- a/redisinsight/ui/src/pages/browser/components/zset-details/ZSetDetails.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/ZSetDetailsTable.tsx @@ -42,7 +42,7 @@ import { StopPropagation } from 'uiSrc/components/virtual-table' import { getColumnWidth } from 'uiSrc/components/virtual-grid' import { decompressingBuffer } from 'uiSrc/utils/decompressors' import { AddMembersToZSetDto, SearchZSetMembersResponse } from 'apiSrc/modules/browser/dto' -import PopoverDelete from '../popover-delete/PopoverDelete' +import PopoverDelete from '../../../../../components/popover-delete/PopoverDelete' import styles from './styles.module.scss' @@ -64,7 +64,7 @@ export interface Props { onRemoveKey: () => void } -const ZSetDetails = (props: Props) => { +const ZSetDetailsTable = (props: Props) => { const { isFooterOpen, onRemoveKey } = props const { loading, searching } = useSelector(zsetSelector) @@ -419,6 +419,7 @@ const ZSetDetails = (props: Props) => { return ( <>
{ ) } -export default ZSetDetails +export { ZSetDetailsTable } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/index.ts new file mode 100644 index 0000000000..b0e76e59ba --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/index.ts @@ -0,0 +1 @@ +export { ZSetDetailsTable } from './ZSetDetailsTable' diff --git a/redisinsight/ui/src/pages/browser/components/zset-details/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/browser/components/zset-details/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/styles.module.scss diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/index.ts b/redisinsight/ui/src/pages/browser/modules/key-details/index.ts new file mode 100644 index 0000000000..2c7e1334c7 --- /dev/null +++ b/redisinsight/ui/src/pages/browser/modules/key-details/index.ts @@ -0,0 +1,5 @@ +import KeyDetails from './KeyDetails' + +export { + KeyDetails, +} diff --git a/redisinsight/ui/src/pages/browser/components/key-details/styles.module.scss b/redisinsight/ui/src/pages/browser/modules/key-details/styles.module.scss similarity index 72% rename from redisinsight/ui/src/pages/browser/components/key-details/styles.module.scss rename to redisinsight/ui/src/pages/browser/modules/key-details/styles.module.scss index 97d78b6108..e7eea19e00 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/modules/key-details/styles.module.scss @@ -1,4 +1,4 @@ -.page { +.container { height: 100%; display: flex; flex-direction: column; @@ -16,7 +16,7 @@ } .content { - height: calc(100% - 220px); + height: 100%; background-color: var(--euiColorEmptyShade); position: relative; > div { @@ -33,15 +33,6 @@ border-bottom-width: 1px !important; } -.placeholder { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - padding: 12px; - width: 100%; -} - :global(.key-details-body) { position: relative; height: calc(100% - 105px); @@ -54,15 +45,15 @@ border-top: 1px solid var(--euiColorLightShade); } -.closeRightPanel { - position: absolute; - top: 22px; - right: 18px; +.actionBtn { + margin-right: 12px; + position: relative; + z-index: 2; - .closeBtn { - :global(svg) { - width: 20px; - height: 20px; + &.withText { + color: var(--euiTextSubduedColor) !important; + :global(.euiButton__text) { + font: normal normal normal 12px/18px Graphik !important; } } } diff --git a/redisinsight/ui/src/pages/home/HomePage.tsx b/redisinsight/ui/src/pages/home/HomePage.tsx index a4c8fc529b..75faed434e 100644 --- a/redisinsight/ui/src/pages/home/HomePage.tsx +++ b/redisinsight/ui/src/pages/home/HomePage.tsx @@ -2,8 +2,9 @@ import { EuiPage, EuiPageBody, EuiResizableContainer, EuiResizeObserver } from ' import React, { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import cx from 'classnames' +import DatabasePanel from 'uiSrc/pages/home/components/database-panel' import { clusterSelector, resetDataRedisCluster, resetInstancesRedisCluster, } from 'uiSrc/slices/instances/cluster' -import { setTitle } from 'uiSrc/utils' +import { Nullable, setTitle } from 'uiSrc/utils' import { PageHeader } from 'uiSrc/components' import { BrowserStorageItem } from 'uiSrc/constants' import { resetKeys } from 'uiSrc/slices/browser/keys' @@ -25,19 +26,22 @@ import { fetchContentAction as fetchCreateRedisButtonsAction } from 'uiSrc/slice import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry' import { appRedirectionSelector, setUrlHandlingInitialState } from 'uiSrc/slices/app/url-handling' import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' -import AddDatabaseContainer, { AddDbType } from './components/AddDatabases/AddDatabasesContainer' -import DatabasesList from './components/DatabasesListComponent/DatabasesListWrapper' -import WelcomeComponent from './components/WelcomeComponent/WelcomeComponent' -import HomeHeader from './components/HomeHeader' +import { AddDbType } from 'uiSrc/pages/home/constants' +import DatabasesList from './components/databases-list-component' +import WelcomeComponent from './components/welcome-component' +import HomeHeader from './components/home-header' import './styles.scss' import styles from './styles.module.scss' +enum RightPanelName { + AddDatabase = 'add', + EditDatabase = 'edit' +} + const HomePage = () => { const [width, setWidth] = useState(0) - const [addDialogIsOpen, setAddDialogIsOpen] = useState(false) - const [editDialogIsOpen, setEditDialogIsOpen] = useState(false) - const [dialogIsOpen, setDialogIsOpen] = useState(false) + const [openRightPanel, setOpenRightPanel] = useState>(null) const [welcomeIsShow, setWelcomeIsShow] = useState( !localStorageService.get(BrowserStorageItem.instancesCount) ) @@ -86,8 +90,7 @@ const HomePage = () => { useEffect(() => { if (isChangedInstance) { - setAddDialogIsOpen(!isChangedInstance) - setEditDialogIsOpen(!isChangedInstance) + setOpenRightPanel(null) dispatch(setEditedInstance(null)) // send page view after adding database from welcome page sendPageViewTelemetry({ @@ -107,29 +110,25 @@ const HomePage = () => { useEffect(() => { if (clusterCredentials || cloudCredentials || sentinelInstance) { - setAddDialogIsOpen(true) + setOpenRightPanel(RightPanelName.AddDatabase) } }, [clusterCredentials, cloudCredentials, sentinelInstance]) useEffect(() => { if (action === UrlHandlingActions.Connect) { - setAddDialogIsOpen(true) + setOpenRightPanel(RightPanelName.AddDatabase) } }, [action, dbConnection]) useEffect(() => { - const isDialogOpen = !!instances.length && (addDialogIsOpen || editDialogIsOpen) - const instancesCashCount = JSON.parse( localStorageService.get(BrowserStorageItem.instancesCount) ?? '0' ) - const isShowWelcome = !instances.length && !addDialogIsOpen && !editDialogIsOpen && !instancesCashCount - - setDialogIsOpen(isDialogOpen) + const isShowWelcome = !instances.length && !openRightPanel && !instancesCashCount setWelcomeIsShow(isShowWelcome) - }, [addDialogIsOpen, editDialogIsOpen, instances, loading]) + }, [openRightPanel, instances, loading]) useEffect(() => { if (editedInstance) { @@ -152,7 +151,7 @@ const HomePage = () => { const closeEditDialog = () => { dispatch(setEditedInstance(null)) - setEditDialogIsOpen(false) + setOpenRightPanel(null) sendEventTelemetry({ event: TelemetryEvent.CONFIG_DATABASES_DATABASE_EDIT_CANCELLED_CLICKED, @@ -166,9 +165,8 @@ const HomePage = () => { dispatch(resetDataRedisCluster()) dispatch(resetDataSentinel()) - setAddDialogIsOpen(false) + setOpenRightPanel(null) dispatch(setEditedInstance(null)) - setEditDialogIsOpen(false) if (action === UrlHandlingActions.Connect) { dispatch(setUrlHandlingInitialState()) @@ -181,22 +179,23 @@ const HomePage = () => { const handleAddInstance = (addDbType = AddDbType.manual) => { initialDbTypeRef.current = addDbType - setAddDialogIsOpen(true) + setOpenRightPanel(RightPanelName.AddDatabase) dispatch(setEditedInstance(null)) - setEditDialogIsOpen(false) } const handleEditInstance = (editedInstance: Instance) => { if (editedInstance) { dispatch(fetchEditedInstanceAction(editedInstance)) - setEditDialogIsOpen(true) - setAddDialogIsOpen(false) + setOpenRightPanel(RightPanelName.EditDatabase) } } const handleDeleteInstances = (instances: Instance[]) => { - if (instances.find((instance) => instance.id === editedInstance?.id)) { + if ( + instances.find((instance) => instance.id === editedInstance?.id) + && openRightPanel === RightPanelName.EditDatabase + ) { dispatch(setEditedInstance(null)) - setEditDialogIsOpen(false) + setOpenRightPanel(null) } instances.forEach((instance) => { @@ -227,7 +226,7 @@ const HomePage = () => { onAddInstance={handleAddInstance} direction="row" /> - {dialogIsOpen ? ( + {openRightPanel && instances.length ? (
{(EuiResizablePanel, EuiResizableButton) => ( @@ -242,7 +241,7 @@ const HomePage = () => {
{ scrollable={false} initialSize={38} className={cx({ - [styles.contentActive]: editDialogIsOpen, + [styles.contentActive]: openRightPanel === RightPanelName.EditDatabase, })} id="form" paddingSize="none" style={{ minWidth: '494px' }} > - {editDialogIsOpen && ( - - )} - - {addDialogIsOpen && ( - )} @@ -297,14 +294,14 @@ const HomePage = () => { ) : ( <> - {addDialogIsOpen && ( - () -const mockedEditedInstance: Instance = { - name: 'name', - host: 'host', - port: 123, - timeout: 10_000, - id: '123', - modules: [], - tls: true, - caCert: { id: 'zxc' }, - clientCert: { id: 'zxc' }, -} - -const mockedValues = { - newCaCert: '', - tls: true, - newCaCertName: '', - selectedCaCertName: '', - tlsClientAuthRequired: false, - verifyServerTlsCert: true, - newTlsCertPairName: '', - selectedTlsClientCertId: '', - newTlsClientCert: '', - newTlsClientKey: '', -} - -jest.mock('./InstanceForm/InstanceForm', () => ({ - __esModule: true, - namedExport: jest.fn(), - default: jest.fn(), -})) - -jest.mock('uiSrc/telemetry', () => ({ - ...jest.requireActual('uiSrc/telemetry'), - sendEventTelemetry: jest.fn(), -})) - -jest.mock('uiSrc/slices/instances/instances', () => ({ - ...jest.requireActual('uiSrc/slices/instances/instances'), - updateInstanceAction: () => jest.fn, - testInstanceStandaloneAction: () => jest.fn, - instancesSelector: jest.fn().mockReturnValue({ loadingChanging: false }), -})) - -jest.mock('uiSrc/slices/instances/clientCerts', () => ({ - clientCertsSelector: () => jest.fn().mockReturnValue({ data: [] }), - fetchClientCerts: jest.fn, -})) - -jest.mock('uiSrc/slices/instances/caCerts', () => ({ - caCertsSelector: () => jest.fn().mockReturnValue({ data: [] }), - fetchCaCerts: () => jest.fn, -})) - -jest.mock('uiSrc/slices/instances/sentinel', () => ({ - sentinelSelector: () => jest.fn().mockReturnValue({ loading: false }), - fetchMastersSentinelAction: () => jest.fn, -})) - -let store: typeof mockedStore -beforeEach(() => { - cleanup() - store = cloneDeep(mockedStore) - store.clearActions() -}) - -const MockInstanceForm = (props: InstanceProps) => ( -
- - - - -
-) - -describe('InstanceFormWrapper', () => { - beforeAll(() => { - InstanceForm.mockImplementation(MockInstanceForm) - }) - it('should render', () => { - expect( - render( - - ) - ).toBeTruthy() - }) - - it('should send prop timeout / 1_000 (in seconds)', () => { - expect( - render( - - ) - ).toBeTruthy() - - expect(InstanceForm).toHaveBeenCalledWith( - expect.objectContaining({ - formFields: expect.objectContaining({ - timeout: toString(mockedEditedInstance?.timeout / 1_000), - }), - }), - {}, - ) - }) - - it('should call onClose', () => { - const onClose = jest.fn() - render( - - ) - fireEvent.click(screen.getByTestId('close-btn')) - expect(onClose).toBeCalled() - }) - - it('should submit with editMode', () => { - const component = render( - - ) - fireEvent.click(screen.getByTestId('submit-form-btn')) - expect(component).toBeTruthy() - }) - - it('should call onHostNamePaste', () => { - const component = render( - - ) - fireEvent.click(screen.getByTestId('paste-hostName-btn')) - expect(component).toBeTruthy() - }) - - it('should call proper telemetry events after click test connection', () => { - const sendEventTelemetryMock = jest.fn() - - sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) - - render( - - ) - fireEvent.click(screen.getByTestId('btn-test-connection')) - expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.CONFIG_DATABASES_TEST_CONNECTION_CLICKED, - }) - sendEventTelemetry.mockRestore() - }) - - it('should call proper actions onSubmit with url handling', () => { - render( - - ) - fireEvent.click(screen.getByTestId('submit-form-btn')) - expect(store.getActions()).toEqual([ - defaultInstanceChanging() - ]) - }) -}) diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceFormWrapper.tsx b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceFormWrapper.tsx deleted file mode 100644 index 9c71b881e7..0000000000 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceFormWrapper.tsx +++ /dev/null @@ -1,650 +0,0 @@ -/* eslint-disable no-nested-ternary */ -import { ConnectionString } from 'connection-string' -import { isUndefined, pick, toNumber, toString, omit } from 'lodash' -import React, { useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router' - -import { - checkConnectToInstanceAction, - createInstanceStandaloneAction, - instancesSelector, - testInstanceStandaloneAction, - updateInstanceAction, - cloneInstanceAction, -} from 'uiSrc/slices/instances/instances' -import { fetchMastersSentinelAction, sentinelSelector, } from 'uiSrc/slices/instances/sentinel' -import { Nullable, removeEmpty, getFormUpdates, transformQueryParamsObject } from 'uiSrc/utils' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { caCertsSelector, fetchCaCerts } from 'uiSrc/slices/instances/caCerts' -import { ConnectionType, Instance, InstanceType, } from 'uiSrc/slices/interfaces' -import { DbType, Pages, REDIS_URI_SCHEMES } from 'uiSrc/constants' -import { clientCertsSelector, fetchClientCerts, } from 'uiSrc/slices/instances/clientCerts' -import { appInfoSelector } from 'uiSrc/slices/app/info' - -import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' -import { appRedirectionSelector, setUrlHandlingInitialState } from 'uiSrc/slices/app/url-handling' -import { getRedirectionPage } from 'uiSrc/utils/routing' -import InstanceForm from './InstanceForm' -import { DbConnectionInfo } from './InstanceForm/interfaces' -import { ADD_NEW, ADD_NEW_CA_CERT, DEFAULT_TIMEOUT, NO_CA_CERT, SshPassType } from './InstanceForm/constants' - -export interface Props { - width: number - isResizablePanel?: boolean - instanceType: InstanceType - editMode: boolean - urlHandlingAction?: Nullable - initialValues?: Nullable> - editedInstance: Nullable - onClose?: () => void - onDbEdited?: () => void - onAliasEdited?: (value: string) => void -} - -export enum SubmitBtnText { - AddDatabase = 'Add Redis Database', - EditDatabase = 'Apply changes', - ConnectToSentinel = 'Discover database', - CloneDatabase = 'Clone Database' -} - -export enum LoadingDatabaseText { - AddDatabase = 'Adding database...', - EditDatabase = 'Editing database...', -} - -export enum TitleDatabaseText { - AddDatabase = 'Add Redis Database', - EditDatabase = 'Edit Redis Database', -} - -const getInitialValues = (editedInstance?: Nullable>) => ({ - // undefined - to show default value, empty string - for existing db - host: editedInstance?.host ?? (editedInstance ? '' : undefined), - port: editedInstance?.port?.toString() ?? (editedInstance ? '' : undefined), - name: editedInstance?.name ?? (editedInstance ? '' : undefined), - username: editedInstance?.username ?? '', - password: editedInstance?.password ?? '', - timeout: editedInstance?.timeout - ? toString(editedInstance?.timeout / 1_000) - : (editedInstance ? '' : undefined), - tls: !!editedInstance?.tls ?? false, - ssh: !!editedInstance?.ssh ?? false, - servername: editedInstance?.tlsServername, - sshPassType: editedInstance?.sshOptions - ? (editedInstance.sshOptions.privateKey ? SshPassType.PrivateKey : SshPassType.Password) - : SshPassType.Password -}) - -const InstanceFormWrapper = (props: Props) => { - const { - editMode, - width, - instanceType, - isResizablePanel = false, - onClose, - onDbEdited, - onAliasEdited, - editedInstance, - urlHandlingAction, - initialValues: initialValuesProp - } = props - const [initialValues, setInitialValues] = useState(getInitialValues(editedInstance || initialValuesProp)) - const [isCloneMode, setIsCloneMode] = useState(false) - - const { host, port, name, username, password, timeout, tls, ssh, sshPassType, servername } = initialValues - - const { loadingChanging: loadingStandalone } = useSelector(instancesSelector) - const { loading: loadingSentinel } = useSelector(sentinelSelector) - const { data: caCertificates } = useSelector(caCertsSelector) - const { data: certificates } = useSelector(clientCertsSelector) - const { server } = useSelector(appInfoSelector) - const { properties: urlHandlingProperties } = useSelector(appRedirectionSelector) - - const tlsClientAuthRequired = !!editedInstance?.clientCert?.id ?? false - const selectedTlsClientCertId = editedInstance?.clientCert?.id ?? ADD_NEW - const verifyServerTlsCert = editedInstance?.verifyServerCert ?? false - const selectedCaCertName = editedInstance?.caCert?.id ?? NO_CA_CERT - const sentinelMasterUsername = editedInstance?.sentinelMaster?.username ?? '' - const sentinelMasterPassword = editedInstance?.sentinelMaster?.password ?? '' - - const connectionType = editedInstance?.connectionType ?? DbType.STANDALONE - const masterName = editedInstance?.sentinelMaster?.name - - const history = useHistory() - const dispatch = useDispatch() - - useEffect(() => { - dispatch(fetchCaCerts()) - dispatch(fetchClientCerts()) - }, []) - - useEffect(() => { - (editedInstance || initialValuesProp) && setInitialValues({ - ...initialValues, - ...getInitialValues(editedInstance || initialValuesProp) - }) - setIsCloneMode(false) - }, [editedInstance, initialValuesProp]) - - const onMastersSentinelFetched = () => { - history.push(Pages.sentinelDatabases) - } - - const handleSuccessConnectWithRedirect = (id: string) => { - const { redirect } = urlHandlingProperties - dispatch(setUrlHandlingInitialState()) - - dispatch(checkConnectToInstanceAction(id, (id) => { - if (redirect) { - const pageToRedirect = getRedirectionPage(redirect, id) - - if (pageToRedirect) { - history.push(pageToRedirect) - } - } - })) - } - - const handleSubmitDatabase = (payload: any) => { - if (isCloneMode && connectionType === ConnectionType.Sentinel) { - dispatch(createInstanceStandaloneAction(payload)) - return - } - - if (instanceType === InstanceType.Sentinel) { - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUBMITTED - }) - - delete payload.name - delete payload.db - dispatch(fetchMastersSentinelAction(payload, onMastersSentinelFetched)) - } else { - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_MANUALLY_SUBMITTED - }) - - if (urlHandlingAction === UrlHandlingActions.Connect) { - const cloudDetails = transformQueryParamsObject( - pick( - urlHandlingProperties, - ['cloudId', 'subscriptionType', 'planMemoryLimit', 'memoryLimitMeasurementUnit', 'free'] - ) - ) - - const db = { ...payload } - if (cloudDetails?.cloudId) { - db.cloudDetails = cloudDetails - } - - dispatch(createInstanceStandaloneAction(db, undefined, handleSuccessConnectWithRedirect)) - return - } - - dispatch( - createInstanceStandaloneAction(payload, onMastersSentinelFetched) - ) - } - } - const handleEditDatabase = (payload: any) => { - dispatch(updateInstanceAction(payload, onDbEdited)) - } - - const handleCloneDatabase = (payload: any) => { - dispatch(cloneInstanceAction(payload)) - } - - const handleUpdateEditingName = (name: string) => { - const requiredFields = [ - 'id', - 'host', - 'port', - 'username', - 'password', - 'tls', - 'sentinelMaster', - ] - const database = pick(editedInstance, ...requiredFields) - dispatch(updateInstanceAction({ ...database, name })) - } - - const handleTestConnectionDatabase = (values: DbConnectionInfo) => { - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_TEST_CONNECTION_CLICKED - }) - const { - name, - host, - port, - username, - password, - db, - compressor, - timeout, - sentinelMasterName, - sentinelMasterUsername, - sentinelMasterPassword, - newCaCert, - tls, - sni, - servername, - newCaCertName, - selectedCaCertName, - tlsClientAuthRequired, - verifyServerTlsCert, - newTlsCertPairName, - selectedTlsClientCertId, - newTlsClientCert, - newTlsClientKey, - } = values - - const tlsSettings = { - useTls: tls, - servername: (sni && servername) || undefined, - verifyServerCert: verifyServerTlsCert, - caCert: - !tls || selectedCaCertName === NO_CA_CERT - ? undefined - : selectedCaCertName === ADD_NEW_CA_CERT - ? { - new: { - name: newCaCertName, - certificate: newCaCert, - }, - } - : { - name: selectedCaCertName, - }, - clientAuth: tls && tlsClientAuthRequired, - clientCert: !tls - ? undefined - : typeof selectedTlsClientCertId === 'string' - && tlsClientAuthRequired - && selectedTlsClientCertId !== ADD_NEW - ? { id: selectedTlsClientCertId } - : selectedTlsClientCertId === ADD_NEW && tlsClientAuthRequired - ? { - new: { - name: newTlsCertPairName, - certificate: newTlsClientCert, - key: newTlsClientKey, - }, - } - : undefined, - } - - const database: any = { - name, - host, - port: +port, - db: +(db || 0), - username, - password, - compressor, - timeout: timeout ? toNumber(timeout) * 1_000 : toNumber(DEFAULT_TIMEOUT), - } - - // add tls & ssh for database (modifies database object) - applyTlSDatabase(database, tlsSettings) - applySSHDatabase(database, values) - - if (isCloneMode && connectionType === ConnectionType.Sentinel) { - database.sentinelMaster = { - name: sentinelMasterName, - username: sentinelMasterUsername, - password: sentinelMasterPassword, - } - } - - if (editMode && editedInstance) { - dispatch(testInstanceStandaloneAction({ - ...getFormUpdates(database, editedInstance), - id: editedInstance.id, - })) - } else { - dispatch(testInstanceStandaloneAction(removeEmpty(database))) - } - } - - const autoFillFormDetails = (content: string): boolean => { - try { - const details = new ConnectionString(content) - - /* If a protocol exists, it should be a redis protocol */ - if (details.protocol && !REDIS_URI_SCHEMES.includes(details.protocol)) return false - /* - * Auto fill logic: - * 1) If the port is parsed, we are sure that the user has indeed copied a connection string. - * '172.18.0.2:12000' => {host: '172,18.0.2', port: 12000} - * 'redis-12000.cluster.local:12000' => {host: 'redis-12000.cluster.local', port: 12000} - * 'lorem ipsum' => {host: undefined, port: undefined} - * 2) If the port is `undefined` but a redis URI scheme is present as protocol, we follow - * the "Scheme semantics" as mentioned in the official URI schemes. - * i) redis:// - https://www.iana.org/assignments/uri-schemes/prov/redis - * ii) rediss:// - https://www.iana.org/assignments/uri-schemes/prov/rediss - */ - if ( - details.port !== undefined - || REDIS_URI_SCHEMES.includes(details.protocol || '') - ) { - setInitialValues({ - name: details.host || name || 'localhost:6379', - host: details.hostname || host || 'localhost', - port: `${details.port || port || 9443}`, - username: details.user || '', - password: details.password, - tls: details.protocol === 'rediss', - ssh: false, - sshPassType: SshPassType.Password - } as any) - /* - * auto fill was successfull so return true - */ - return true - } - } catch (err) { - /* The pasted content is not a connection URI so ignore. */ - return false - } - return false - } - - const applyTlSDatabase = (database: any, tlsSettings: any) => { - const { useTls, verifyServerCert, servername, caCert, clientAuth, clientCert } = tlsSettings - if (!useTls) return - - database.tls = useTls - database.tlsServername = servername - database.verifyServerCert = !!verifyServerCert - - if (!isUndefined(caCert?.new)) { - database.caCert = { - name: caCert?.new.name, - certificate: caCert?.new.certificate, - } - } - - if (!isUndefined(caCert?.name)) { - database.caCert = { id: caCert?.name } - } - - if (clientAuth) { - if (!isUndefined(clientCert.new)) { - database.clientCert = { - name: clientCert.new.name, - certificate: clientCert.new.certificate, - key: clientCert.new.key, - } - } - - if (!isUndefined(clientCert.id)) { - database.clientCert = { id: clientCert.id } - } - } - } - - const applySSHDatabase = (database: any, values: DbConnectionInfo) => { - const { - ssh, - sshPassType, - sshHost, - sshPort, - sshPassword, - sshUsername, - sshPassphrase, - sshPrivateKey, - } = values - - if (ssh) { - database.ssh = true - database.sshOptions = { - host: sshHost, - port: +sshPort, - username: sshUsername, - } - - if (sshPassType === SshPassType.Password) { - database.sshOptions.password = sshPassword - database.sshOptions.passphrase = null - database.sshOptions.privateKey = null - } - - if (sshPassType === SshPassType.PrivateKey) { - database.sshOptions.password = null - database.sshOptions.passphrase = sshPassphrase - database.sshOptions.privateKey = sshPrivateKey - } - } - } - - const editDatabase = (tlsSettings: any, values: DbConnectionInfo, isCloneMode: boolean) => { - const { - name, - host, - port, - db, - username, - password, - timeout, - compressor, - sentinelMasterUsername, - sentinelMasterPassword, - } = values - - const database: any = { - id: editedInstance?.id, - name, - host, - port: +port, - db: +(db || 0), - username, - password, - compressor, - timeout: timeout ? toNumber(timeout) * 1_000 : toNumber(DEFAULT_TIMEOUT), - } - - // add tls & ssh for database (modifies database object) - applyTlSDatabase(database, tlsSettings) - applySSHDatabase(database, values) - - if (connectionType === ConnectionType.Sentinel) { - database.sentinelMaster = {} - database.sentinelMaster.name = masterName - database.sentinelMaster.username = sentinelMasterUsername - database.sentinelMaster.password = sentinelMasterPassword - } - - const payload = getFormUpdates(database, omit(editedInstance, ['id'])) - if (isCloneMode) { - handleCloneDatabase(payload) - } else { - handleEditDatabase(payload) - } - } - - const addDatabase = (tlsSettings: any, values: DbConnectionInfo) => { - const { - name, - host, - port, - username, - password, - timeout, - db, - compressor, - sentinelMasterName, - sentinelMasterUsername, - sentinelMasterPassword, - } = values - const database: any = { - name, - host, - port: +port, - db: +(db || 0), - compressor, - username, - password, - timeout: timeout ? toNumber(timeout) * 1_000 : toNumber(DEFAULT_TIMEOUT), - } - - // add tls & ssh for database (modifies database object) - applyTlSDatabase(database, tlsSettings) - applySSHDatabase(database, values) - - if (isCloneMode && connectionType === ConnectionType.Sentinel) { - database.sentinelMaster = { - name: sentinelMasterName, - username: sentinelMasterUsername, - password: sentinelMasterPassword, - } - } - - handleSubmitDatabase(removeEmpty(database)) - } - - const handleConnectionFormSubmit = (values: DbConnectionInfo) => { - const { - newCaCert, - tls, - sni, - servername, - newCaCertName, - selectedCaCertName, - tlsClientAuthRequired, - verifyServerTlsCert, - newTlsCertPairName, - selectedTlsClientCertId, - newTlsClientCert, - newTlsClientKey, - } = values - - const tlsSettings = { - useTls: tls, - servername: (sni && servername) || undefined, - verifyServerCert: verifyServerTlsCert, - caCert: - !tls || selectedCaCertName === NO_CA_CERT - ? undefined - : selectedCaCertName === ADD_NEW_CA_CERT - ? { - new: { - name: newCaCertName, - certificate: newCaCert, - }, - } - : { - name: selectedCaCertName, - }, - clientAuth: tls && tlsClientAuthRequired, - clientCert: !tls - ? undefined - : typeof selectedTlsClientCertId === 'string' - && tlsClientAuthRequired - && selectedTlsClientCertId !== ADD_NEW - ? { id: selectedTlsClientCertId } - : selectedTlsClientCertId === ADD_NEW && tlsClientAuthRequired - ? { - new: { - name: newTlsCertPairName, - certificate: newTlsClientCert, - key: newTlsClientKey, - }, - } - : undefined, - } - - if (editMode) { - editDatabase(tlsSettings, values, isCloneMode) - } else { - addDatabase(tlsSettings, values) - } - } - - const handleOnClose = () => { - dispatch(setUrlHandlingInitialState()) - - if (isCloneMode) { - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED, - eventData: { - databaseId: editedInstance?.id, - } - }) - } - onClose?.() - } - - const connectionFormData = { - ...editedInstance, - name, - host, - port, - tls, - username, - password, - timeout, - connectionType, - tlsClientAuthRequired, - certificates, - selectedTlsClientCertId, - caCertificates, - verifyServerTlsCert, - selectedCaCertName, - sentinelMasterUsername, - sentinelMasterPassword, - ssh, - sshPassType, - servername, - } - - const getSubmitButtonText = () => { - if (instanceType === InstanceType.Sentinel) { - return SubmitBtnText.ConnectToSentinel - } - if (isCloneMode) { - return SubmitBtnText.CloneDatabase - } - if (editMode) { - return SubmitBtnText.EditDatabase - } - return SubmitBtnText.AddDatabase - } - - return ( -
- -
- ) -} - -export default InstanceFormWrapper diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/index.ts b/redisinsight/ui/src/pages/home/components/AddInstanceForm/index.ts deleted file mode 100644 index df91655e04..0000000000 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import InstanceFormWrapper from './InstanceFormWrapper' - -export default InstanceFormWrapper diff --git a/redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionFormWrapper.spec.tsx b/redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionFormWrapper.spec.tsx rename to redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionFormWrapper.tsx b/redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.tsx similarity index 95% rename from redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionFormWrapper.tsx rename to redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.tsx index 5436af2f92..71f07cd700 100644 --- a/redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionFormWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.tsx @@ -8,7 +8,7 @@ import { useResizableFormField } from 'uiSrc/services' import { resetErrors } from 'uiSrc/slices/app/notifications' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import CloudConnectionForm from './CloudConnectionForm/CloudConnectionForm' +import CloudConnectionForm from './cloud-connection-form' export interface Props { width: number diff --git a/redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionForm/CloudConnectionForm.spec.tsx b/redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/CloudConnectionForm.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionForm/CloudConnectionForm.spec.tsx rename to redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/CloudConnectionForm.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionForm/CloudConnectionForm.tsx b/redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/CloudConnectionForm.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/CloudConnection/CloudConnectionForm/CloudConnectionForm.tsx rename to redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/CloudConnectionForm.tsx diff --git a/redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/index.ts b/redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/index.ts new file mode 100644 index 0000000000..0c05734c8c --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/index.ts @@ -0,0 +1,3 @@ +import CloudConnectionForm from './CloudConnectionForm' + +export default CloudConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/cloud-connection/index.ts b/redisinsight/ui/src/pages/home/components/cloud-connection/index.ts new file mode 100644 index 0000000000..90a70f2f19 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/cloud-connection/index.ts @@ -0,0 +1,3 @@ +import CloudConnectionFormWrapper from './CloudConnectionFormWrapper' + +export default CloudConnectionFormWrapper diff --git a/redisinsight/ui/src/pages/home/components/CloudConnection/styles.module.scss b/redisinsight/ui/src/pages/home/components/cloud-connection/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/CloudConnection/styles.module.scss rename to redisinsight/ui/src/pages/home/components/cloud-connection/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.spec.tsx b/redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.spec.tsx similarity index 93% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.spec.tsx rename to redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.spec.tsx index a0569dca26..6d080282b9 100644 --- a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.spec.tsx @@ -1,14 +1,15 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import ClusterConnectionForm, { Props as ClusterConnectionFormProps } from + './cluster-connection-form/ClusterConnectionForm' import ClusterConnectionFormWrapper, { Props, } from './ClusterConnectionFormWrapper' -import ClusterConnectionForm, { Props as ClusterConnectionFormProps } from './ClusterConnectionForm/ClusterConnectionForm' const mockedProps = mock() -jest.mock('./ClusterConnectionForm/ClusterConnectionForm', () => ({ +jest.mock('./cluster-connection-form/ClusterConnectionForm', () => ({ __esModule: true, namedExport: jest.fn(), default: jest.fn(), diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.tsx b/redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.tsx similarity index 52% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.tsx rename to redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.tsx index 2f2d590be5..38379a24b7 100644 --- a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionFormWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { ConnectionString } from 'connection-string' import { useHistory } from 'react-router-dom' import { clusterSelector, fetchInstancesRedisCluster, } from 'uiSrc/slices/instances/cluster' -import { REDIS_URI_SCHEMES, Pages } from 'uiSrc/constants' +import { Pages } from 'uiSrc/constants' import { useResizableFormField } from 'uiSrc/services' import { resetErrors } from 'uiSrc/slices/app/notifications' -import { ICredentialsRedisCluster } from 'uiSrc/slices/interfaces' +import { ICredentialsRedisCluster, InstanceType } from 'uiSrc/slices/interfaces' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { autoFillFormDetails } from 'uiSrc/pages/home/utils' -import ClusterConnectionForm from './ClusterConnectionForm/ClusterConnectionForm' +import ClusterConnectionForm from './cluster-connection-form/ClusterConnectionForm' export interface Props { width: number; @@ -70,44 +70,9 @@ const ClusterConnectionFormWrapper = ({ onClose, width }: Props) => { history.push(Pages.redisEnterpriseAutodiscovery) } - const autoFillFormDetails = (content: string): boolean => { - try { - const details = new ConnectionString(content) - - /* If a protocol exists, it should be a redis protocol */ - if (details.protocol && !REDIS_URI_SCHEMES.includes(details.protocol)) return false - /* - * Auto fill logic: - * 1) If the port is parsed, we are sure that the user has indeed copied a connection string. - * '172.18.0.2:12000' => {host: '172,18.0.2', port: 12000} - * 'redis-12000.cluster.local:12000' => {host: 'redis-12000.cluster.local', port: 12000} - * 'lorem ipsum' => {host: undefined, port: undefined} - * 2) If the port is `undefined` but a redis URI scheme is present as protocol, we follow - * the "Scheme semantics" as mentioned in the official URI schemes. - * i) redis:// - https://www.iana.org/assignments/uri-schemes/prov/redis - * ii) rediss:// - https://www.iana.org/assignments/uri-schemes/prov/rediss - */ - if ( - details.port !== undefined - || REDIS_URI_SCHEMES.includes(details.protocol || '') - ) { - setInitialValues({ - host: details.hostname || initialValues.host || 'localhost', - port: `${details.port || initialValues.port || 9443}`, - username: details.user || '', - password: details.password || '', - }) - /* - * auto fill was successfull so return true - */ - return true - } - } catch (err) { - /* The pasted content is not a connection URI so ignore. */ - return false - } - return false - } + const handlePostHostName = (content: string) => ( + autoFillFormDetails(content, initialValues, setInitialValues, InstanceType.RedisEnterpriseCluster) + ) return (
@@ -117,7 +82,7 @@ const ClusterConnectionFormWrapper = ({ onClose, width }: Props) => { username={credentials?.username ?? ''} password={credentials?.password ?? ''} initialValues={initialValues} - onHostNamePaste={autoFillFormDetails} + onHostNamePaste={handlePostHostName} flexGroupClassName={flexGroupClassName} flexItemClassName={flexItemClassName} onClose={onClose} diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.spec.tsx b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.spec.tsx rename to redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.tsx b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.tsx rename to redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.tsx diff --git a/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/index.ts b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/index.ts new file mode 100644 index 0000000000..44ec2d4262 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/index.ts @@ -0,0 +1,3 @@ +import ClusterConnectionForm from './ClusterConnectionForm' + +export default ClusterConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/cluster-connection/index.ts b/redisinsight/ui/src/pages/home/components/cluster-connection/index.ts new file mode 100644 index 0000000000..405d374c5f --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/cluster-connection/index.ts @@ -0,0 +1,3 @@ +import ClusterConnectionFormWrapper from './ClusterConnectionFormWrapper' + +export default ClusterConnectionFormWrapper diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/styles.module.scss b/redisinsight/ui/src/pages/home/components/cluster-connection/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/styles.module.scss rename to redisinsight/ui/src/pages/home/components/cluster-connection/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/types.ts b/redisinsight/ui/src/pages/home/components/cluster-connection/types.ts similarity index 100% rename from redisinsight/ui/src/pages/home/components/ClusterConnection/types.ts rename to redisinsight/ui/src/pages/home/components/cluster-connection/types.ts diff --git a/redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.spec.tsx b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.spec.tsx similarity index 61% rename from redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.spec.tsx rename to redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.spec.tsx index 87cc8a70db..889a36bd66 100644 --- a/redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.spec.tsx @@ -22,25 +22,6 @@ describe('DatabaseAlias', () => { expect(render()).toBeTruthy() }) - it('should call onApplyChanges on edit alias', () => { - const onApply = jest.fn() - render() - - fireEvent.click(screen.getByTestId('edit-alias-btn')) - fireEvent.change(screen.getByTestId('alias-input'), { target: { value: 'alias' } }) - fireEvent.submit(screen.getByTestId('alias-input')) - - expect(onApply).toHaveBeenCalledWith('alias', expect.anything(), expect.anything()) - }) - - it('should call onOpen', () => { - const onOpen = jest.fn() - render() - - fireEvent.click(screen.getByTestId('connect-to-db-btn')) - expect(onOpen).toHaveBeenCalled() - }) - it('should not render part of content in edit mode', () => { render() @@ -48,14 +29,6 @@ describe('DatabaseAlias', () => { expect(screen.queryByTestId('db-alias')).toHaveTextContent('alias') }) - it('should call onCloneBack in clone mode', () => { - const onCloneBack = jest.fn() - render() - - fireEvent.click(screen.getByTestId('back-btn')) - expect(onCloneBack).toHaveBeenCalled() - }) - it('should render icon for redis-stack', () => { render() diff --git a/redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.tsx b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx similarity index 74% rename from redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.tsx rename to redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx index 207270b79a..52a726911f 100644 --- a/redisinsight/ui/src/pages/home/components/DatabaseAlias/DatabaseAlias.tsx +++ b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx @@ -11,12 +11,15 @@ import { EuiToolTip, } from '@elastic/eui' import cx from 'classnames' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { toNumber } from 'lodash' +import { useHistory } from 'react-router' + +import { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module' import { BuildType } from 'uiSrc/constants/env' import { appInfoSelector } from 'uiSrc/slices/app/info' import { Nullable, getDbIndex } from 'uiSrc/utils' -import { Theme } from 'uiSrc/constants' +import { PageNames, Pages, Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' import RediStackDarkMin from 'uiSrc/assets/img/modules/redistack/RediStackDark-min.svg' @@ -24,29 +27,52 @@ import RediStackLightMin from 'uiSrc/assets/img/modules/redistack/RediStackLight import RediStackLightLogo from 'uiSrc/assets/img/modules/redistack/RedisStackLogoLight.svg' import RediStackDarkLogo from 'uiSrc/assets/img/modules/redistack/RedisStackLogoDark.svg' +import { getRedisModulesSummary, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { + changeInstanceAliasAction, + checkConnectToInstanceAction, + setConnectedInstanceId +} from 'uiSrc/slices/instances/instances' +import { resetKeys } from 'uiSrc/slices/browser/keys' +import { appContextSelector, setAppContextInitialState } from 'uiSrc/slices/app/context' import styles from './styles.module.scss' export interface Props { alias: string - database?: Nullable - onOpen: () => void - onClone: () => void - onCloneBack: () => void + database?: Nullable isLoading: boolean - onApplyChanges: (value: string, onSuccess?: () => void, onFail?: () => void) => void + onAliasEdited?: (value: string) => void isRediStack?: boolean isCloneMode: boolean + id?: string + provider?: string + setIsCloneMode: (value: boolean) => void + modules: AdditionalRedisModule[] } const DatabaseAlias = (props: Props) => { - const { alias, database, onOpen, onClone, onCloneBack, onApplyChanges, isLoading, isRediStack, isCloneMode } = props + const { + alias, + database, + id, + provider, + onAliasEdited, + isLoading, + isRediStack, + isCloneMode, + setIsCloneMode, + modules, + } = props const { server } = useSelector(appInfoSelector) + const { contextInstanceId, lastPage } = useSelector(appContextSelector) const [isEditing, setIsEditing] = useState(false) const [value, setValue] = useState(alias) const { theme } = useContext(ThemeContext) + const history = useHistory() + const dispatch = useDispatch() useEffect(() => { setValue(alias) @@ -60,21 +86,68 @@ const DatabaseAlias = (props: Props) => { isEditing && setValue(value) } + const connectToInstance = () => { + if (contextInstanceId && contextInstanceId !== id) { + dispatch(resetKeys()) + dispatch(setAppContextInitialState()) + } + dispatch(setConnectedInstanceId(id ?? '')) + + if (lastPage === PageNames.workbench && contextInstanceId === id) { + history.push(Pages.workbench(id)) + return + } + history.push(Pages.browser(id ?? '')) + } + const handleOpen = (event: any) => { event.stopPropagation() event.preventDefault() - onOpen() + const modulesSummary = getRedisModulesSummary(modules) + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_OPEN_DATABASE_BUTTON_CLICKED, + eventData: { + databaseId: id, + provider, + ...modulesSummary, + } + }) + dispatch(checkConnectToInstanceAction(id, connectToInstance)) + // onOpen() } const handleClone = (e: React.MouseEvent) => { e.stopPropagation() e.preventDefault() - onClone() + setIsCloneMode(true) + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_REQUESTED, + eventData: { + databaseId: id + } + }) } const handleApplyChanges = () => { setIsEditing(false) - onApplyChanges(value, () => {}, () => setValue(alias)) + dispatch(changeInstanceAliasAction( + id, + value, + () => { + onAliasEdited?.(value) + }, + () => setValue(alias) + )) + } + + const handleCloneBack = () => { + setIsCloneMode(false) + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED, + eventData: { + databaseId: id + } + }) } const handleDeclineChanges = (event?: React.MouseEvent) => { @@ -89,7 +162,7 @@ const DatabaseAlias = (props: Props) => { {isCloneMode && ( () -describe('AddDatabasesContainer', () => { +describe('DatabasePanel', () => { it('should render', () => { expect( - render() + render() ).toBeTruthy() }) it('should render instance types after click on auto discover', () => { - render() + render() fireEvent.click(screen.getByTestId('add-auto')) expect(screen.getByTestId('db-types')).toBeInTheDocument() }) diff --git a/redisinsight/ui/src/pages/home/components/AddDatabases/AddDatabasesContainer.tsx b/redisinsight/ui/src/pages/home/components/database-panel/DatabasePanel.tsx similarity index 79% rename from redisinsight/ui/src/pages/home/components/AddDatabases/AddDatabasesContainer.tsx rename to redisinsight/ui/src/pages/home/components/database-panel/DatabasePanel.tsx index 4d06fd40c1..f0b650a409 100644 --- a/redisinsight/ui/src/pages/home/components/AddDatabases/AddDatabasesContainer.tsx +++ b/redisinsight/ui/src/pages/home/components/database-panel/DatabasePanel.tsx @@ -20,10 +20,12 @@ import { sentinelSelector, resetDataSentinel } from 'uiSrc/slices/instances/sent import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' import { appRedirectionSelector, setUrlHandlingInitialState } from 'uiSrc/slices/app/url-handling' -import InstanceConnections from './InstanceConnections/InstanceConnections' -import InstanceFormWrapper from '../AddInstanceForm/InstanceFormWrapper' -import ClusterConnectionFormWrapper from '../ClusterConnection/ClusterConnectionFormWrapper' -import CloudConnectionFormWrapper from '../CloudConnection/CloudConnectionFormWrapper' +import { AddDbType } from 'uiSrc/pages/home/constants' +import ClusterConnectionFormWrapper from 'uiSrc/pages/home/components/cluster-connection' +import CloudConnectionFormWrapper from 'uiSrc/pages/home/components/cloud-connection' +import SentinelConnectionWrapper from 'uiSrc/pages/home/components/sentinel-connection' +import ManualConnectionWrapper from 'uiSrc/pages/home/components/manual-connection' +import InstanceConnections from 'uiSrc/pages/home/components/database-panel/instance-connections' import styles from './styles.module.scss' @@ -41,12 +43,7 @@ export interface Props { initConnectionType?: AddDbType } -export enum AddDbType { - manual, - auto, -} - -const AddDatabasesContainer = React.memo((props: Props) => { +const DatabasePanel = React.memo((props: Props) => { const { editMode, isResizablePanel, @@ -92,6 +89,12 @@ const AddDatabasesContainer = React.memo((props: Props) => { } }, [action, dbConnection]) + useEffect(() => { + if (editMode) { + setConnectionType(AddDbType.manual) + } + }, [editMode]) + useEffect(() => // ComponentWillUnmount () => { @@ -183,15 +186,12 @@ const AddDatabasesContainer = React.memo((props: Props) => { const Form = () => ( <> {connectionType === AddDbType.manual && ( - + )} {connectionType === AddDbType.auto && ( <> {typeSelected === InstanceType.Sentinel && ( - + )} {typeSelected === InstanceType.RedisEnterpriseCluster && ( @@ -208,29 +208,29 @@ const AddDatabasesContainer = React.memo((props: Props) => { <>
{!isFullWidth && onClose && ( - - - + + + )} {!editMode && ( - <> - -

Discover and Add Redis Databases

-
- - {connectionType === AddDbType.auto && } - + <> + +

Discover and Add Redis Databases

+
+ + {connectionType === AddDbType.auto && } + )} {Form()}
@@ -239,4 +239,4 @@ const AddDatabasesContainer = React.memo((props: Props) => { ) }) -export default AddDatabasesContainer +export default DatabasePanel diff --git a/redisinsight/ui/src/pages/home/components/database-panel/index.ts b/redisinsight/ui/src/pages/home/components/database-panel/index.ts new file mode 100644 index 0000000000..18eecda936 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/database-panel/index.ts @@ -0,0 +1,3 @@ +import DatabasePanel from './DatabasePanel' + +export default DatabasePanel diff --git a/redisinsight/ui/src/pages/home/components/AddDatabases/InstanceConnections/InstanceConnections.spec.tsx b/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/InstanceConnections.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/AddDatabases/InstanceConnections/InstanceConnections.spec.tsx rename to redisinsight/ui/src/pages/home/components/database-panel/instance-connections/InstanceConnections.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/AddDatabases/InstanceConnections/InstanceConnections.tsx b/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/InstanceConnections.tsx similarity index 98% rename from redisinsight/ui/src/pages/home/components/AddDatabases/InstanceConnections/InstanceConnections.tsx rename to redisinsight/ui/src/pages/home/components/database-panel/instance-connections/InstanceConnections.tsx index dc7e73e4d0..a5703dcf5d 100644 --- a/redisinsight/ui/src/pages/home/components/AddDatabases/InstanceConnections/InstanceConnections.tsx +++ b/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/InstanceConnections.tsx @@ -13,8 +13,7 @@ import LightActiveManualSvg from 'uiSrc/assets/img/light_theme/active_manual.svg import LightNotActiveManualSvg from 'uiSrc/assets/img/light_theme/n_active_manual.svg' import LightActiveAutoSvg from 'uiSrc/assets/img/light_theme/active_auto.svg' import LightNotActiveAutoSvg from 'uiSrc/assets/img/light_theme/n_active_auto.svg' - -import { AddDbType } from '../AddDatabasesContainer' +import { AddDbType } from 'uiSrc/pages/home/constants' import styles from '../styles.module.scss' diff --git a/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/index.ts b/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/index.ts new file mode 100644 index 0000000000..162026205d --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/database-panel/instance-connections/index.ts @@ -0,0 +1,3 @@ +import InstanceConnections from './InstanceConnections' + +export default InstanceConnections diff --git a/redisinsight/ui/src/pages/home/components/AddDatabases/styles.module.scss b/redisinsight/ui/src/pages/home/components/database-panel/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/AddDatabases/styles.module.scss rename to redisinsight/ui/src/pages/home/components/database-panel/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.spec.tsx similarity index 97% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.spec.tsx index 9cffb5a5a7..c1f6d74d86 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.spec.tsx @@ -11,11 +11,11 @@ import { RootState, store } from 'uiSrc/slices/store' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { errorHandlers } from 'uiSrc/mocks/res/responseComposition' import DatabasesListWrapper, { Props } from './DatabasesListWrapper' -import DatabasesList, { Props as DatabasesListProps } from './DatabasesList/DatabasesList' +import DatabasesList, { Props as DatabasesListProps } from './databases-list/DatabasesList' const mockedProps = mock() -jest.mock('./DatabasesList/DatabasesList', () => ({ +jest.mock('./databases-list/DatabasesList', () => ({ __esModule: true, namedExport: jest.fn(), default: jest.fn(), diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx b/redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.tsx similarity index 99% rename from redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx rename to redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.tsx index c163666a4e..4021271d0e 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/databases-list-component/DatabasesListWrapper.tsx @@ -40,7 +40,7 @@ import RediStackLightLogo from 'uiSrc/assets/img/modules/redistack/RedisStackLog import RediStackDarkLogo from 'uiSrc/assets/img/modules/redistack/RedisStackLogoDark.svg' import { ReactComponent as CloudLinkIcon } from 'uiSrc/assets/img/oauth/cloud_link.svg' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' -import DatabasesList from './DatabasesList/DatabasesList' +import DatabasesList from './databases-list' import styles from './styles.module.scss' @@ -363,7 +363,7 @@ const DatabasesListWrapper = ({ <> {instance.cloudDetails && ( - isEditMode: boolean - isCloneMode: boolean onHostNamePaste: (content: string) => boolean - instanceType: InstanceType - connectionType?: ConnectionType - isFromCloud: boolean + showFields: IShowFields + autoFocus?: boolean + passwordType?: IPasswordType } const DatabaseForm = (props: Props) => { @@ -35,12 +47,10 @@ const DatabaseForm = (props: Props) => { flexGroupClassName = '', flexItemClassName = '', formik, - isEditMode, - isCloneMode, onHostNamePaste, - instanceType, - connectionType, - isFromCloud, + autoFocus = false, + showFields, + passwordType = IPasswordType.Password, } = props const { server } = useSelector(appInfoSelector) @@ -84,11 +94,11 @@ const DatabaseForm = (props: Props) => { return ( <> - {(!isEditMode || isCloneMode) && !isFromCloud && ( + {showFields.host && ( { )} - {server?.buildType !== BuildType.RedisStack && !isFromCloud && ( + {server?.buildType !== BuildType.RedisStack && showFields.port && ( { )} - {( - (!isEditMode || isCloneMode) - && instanceType !== InstanceType.Sentinel - && connectionType !== ConnectionType.Sentinel - ) && ( + {showFields.alias && ( @@ -179,7 +185,7 @@ const DatabaseForm = (props: Props) => { { - {connectionType !== ConnectionType.Sentinel && instanceType !== InstanceType.Sentinel && ( + {showFields.timeout && ( () + +jest.mock('./manual-connection-form/ManualConnectionForm', () => ({ + __esModule: true, + namedExport: jest.fn(), + default: jest.fn(), +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +const mockManualConnectionFrom = (props: ManualConnectionFromProps) => ( +
+ + + + +
+) + +describe('ManualConnectionWrapper', () => { + beforeAll(() => { + ManualConnectionFrom.mockImplementation(mockManualConnectionFrom) + }) + it('should render', () => { + expect( + render() + ).toBeTruthy() + }) + + it('should call onHostNamePaste', () => { + const component = render() + fireEvent.click(screen.getByTestId('onHostNamePaste-btn')) + expect(component).toBeTruthy() + }) + + it('should call onClose', () => { + const onClose = jest.fn() + render() + fireEvent.click(screen.getByTestId('onClose-btn')) + expect(onClose).toBeCalled() + }) + + it('should have add database submit button', () => { + render() + expect(screen.getByTestId('btn-submit')).toHaveTextContent(SubmitBtnText.AddDatabase) + }) + + it('should have edit database submit button', () => { + render() + expect(screen.getByTestId('btn-submit')).toHaveTextContent(SubmitBtnText.EditDatabase) + }) + + it('should have edit database submit button', () => { + render() + act(() => { + fireEvent.click(screen.getByTestId('onClone-btn')) + }) + expect(screen.getByTestId('btn-submit')).toHaveTextContent(SubmitBtnText.CloneDatabase) + }) + + it('should call proper telemetry event on Add database', () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + sendEventTelemetry.mockRestore() + render() + act(() => { + fireEvent.click(screen.getByTestId('btn-submit')) + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.CONFIG_DATABASES_MANUALLY_SUBMITTED, + }) + }) + + it('should call proper telemetry event on Clone database', () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + sendEventTelemetry.mockRestore() + render() + act(() => { + fireEvent.click(screen.getByTestId('onClone-btn')) + }) + act(() => { + fireEvent.click(screen.getByTestId('onClose-btn')) + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED, + eventData: { databaseId: undefined } + }) + }) +}) diff --git a/redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx b/redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx new file mode 100644 index 0000000000..2efff9a03d --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx @@ -0,0 +1,262 @@ +import { pick, toNumber, omit } from 'lodash' +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router' + +import { + checkConnectToInstanceAction, + createInstanceStandaloneAction, + instancesSelector, + testInstanceStandaloneAction, + updateInstanceAction, + cloneInstanceAction, +} from 'uiSrc/slices/instances/instances' +import { Nullable, removeEmpty, getFormUpdates, transformQueryParamsObject, getDiffKeysOfObjectValues } from 'uiSrc/utils' +import { BuildType } from 'uiSrc/constants/env' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { fetchCaCerts } from 'uiSrc/slices/instances/caCerts' +import { ConnectionType, Instance, InstanceType } from 'uiSrc/slices/interfaces' +import { DbType, Pages } from 'uiSrc/constants' +import { fetchClientCerts, } from 'uiSrc/slices/instances/clientCerts' +import { appInfoSelector } from 'uiSrc/slices/app/info' +import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' +import { appRedirectionSelector, setUrlHandlingInitialState } from 'uiSrc/slices/app/url-handling' +import { getRedirectionPage } from 'uiSrc/utils/routing' +import { DbConnectionInfo } from 'uiSrc/pages/home/interfaces' +import { applyTlSDatabase, applySSHDatabase, autoFillFormDetails, getTlsSettings, getFormValues } from 'uiSrc/pages/home/utils' +import { + DEFAULT_TIMEOUT, + SubmitBtnText, +} from 'uiSrc/pages/home/constants' +import ManualConnectionForm from './manual-connection-form' + +export interface Props { + width: number + editMode: boolean + urlHandlingAction?: Nullable + initialValues?: Nullable> + editedInstance: Nullable + onClose?: () => void + onDbEdited?: () => void + onAliasEdited?: (value: string) => void +} + +const ManualConnectionWrapper = (props: Props) => { + const { + editMode, + width, + onClose, + onDbEdited, + onAliasEdited, + editedInstance, + urlHandlingAction, + initialValues: initialValuesProp + } = props + const [formFields, setFormFields] = useState(getFormValues(editedInstance || initialValuesProp)) + + const [isCloneMode, setIsCloneMode] = useState(false) + + const { loadingChanging: loadingStandalone } = useSelector(instancesSelector) + const { server } = useSelector(appInfoSelector) + const { properties: urlHandlingProperties } = useSelector(appRedirectionSelector) + + const connectionType = editedInstance?.connectionType ?? DbType.STANDALONE + + const history = useHistory() + const dispatch = useDispatch() + + useEffect(() => { + dispatch(fetchCaCerts()) + dispatch(fetchClientCerts()) + }, []) + + useEffect(() => { + setFormFields(getFormValues(editedInstance || initialValuesProp)) + setIsCloneMode(false) + }, [editedInstance, initialValuesProp]) + + const onMastersSentinelFetched = () => { + history.push(Pages.sentinelDatabases) + } + + const handleSuccessConnectWithRedirect = (id: string) => { + const { redirect } = urlHandlingProperties + dispatch(setUrlHandlingInitialState()) + + dispatch(checkConnectToInstanceAction(id, (id) => { + if (redirect) { + const pageToRedirect = getRedirectionPage(redirect, id) + + if (pageToRedirect) { + history.push(pageToRedirect) + } + } + })) + } + + const handleAddDatabase = (payload: any) => { + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_MANUALLY_SUBMITTED + }) + + if (urlHandlingAction === UrlHandlingActions.Connect) { + const cloudDetails = transformQueryParamsObject( + pick( + urlHandlingProperties, + ['cloudId', 'subscriptionType', 'planMemoryLimit', 'memoryLimitMeasurementUnit', 'free'] + ) + ) + + const db = { ...payload } + if (cloudDetails?.cloudId) { + db.cloudDetails = cloudDetails + } + + dispatch(createInstanceStandaloneAction(db, undefined, handleSuccessConnectWithRedirect)) + return + } + + dispatch(createInstanceStandaloneAction(payload, onMastersSentinelFetched)) + } + const handleEditDatabase = (payload: any) => { + dispatch(updateInstanceAction(payload, onDbEdited)) + } + + const handleCloneDatabase = (payload: any) => { + dispatch(cloneInstanceAction(payload)) + } + + const handleTestConnectionDatabase = (values: DbConnectionInfo) => { + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_TEST_CONNECTION_CLICKED + }) + const payload = preparePayload(values) + + dispatch(testInstanceStandaloneAction(payload)) + } + + const handleConnectionFormSubmit = (values: DbConnectionInfo) => { + if (isCloneMode) { + const diffKeys = getDiffKeysOfObjectValues(formFields, values) + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CONFIRMED, + eventData: { + fieldsModified: diffKeys + } + }) + } + const payload = preparePayload(values) + + if (isCloneMode) { + handleCloneDatabase(payload) + return + } + if (editMode) { + handleEditDatabase(payload) + return + } + + handleAddDatabase(payload) + } + + const preparePayload = (values: any) => { + const tlsSettings = getTlsSettings(values) + + const { + name, + host, + port, + db, + username, + password, + timeout, + compressor, + sentinelMasterName, + sentinelMasterUsername, + sentinelMasterPassword, + } = values + + const database: any = { + id: editedInstance?.id, + name, + host, + port: +port, + db: +(db || 0), + username, + password, + compressor, + timeout: timeout ? toNumber(timeout) * 1_000 : toNumber(DEFAULT_TIMEOUT), + } + + // add tls & ssh for database (modifies database object) + applyTlSDatabase(database, tlsSettings) + applySSHDatabase(database, values) + + if (connectionType === ConnectionType.Sentinel) { + database.sentinelMaster = { + name: sentinelMasterName, + username: sentinelMasterUsername, + password: sentinelMasterPassword, + } + } + + if (editMode) { + database.id = editedInstance?.id + + return getFormUpdates(database, omit(editedInstance, ['id'])) + } + + return removeEmpty(database) + } + + const handleOnClose = () => { + dispatch(setUrlHandlingInitialState()) + + if (isCloneMode) { + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED, + eventData: { + databaseId: editedInstance?.id, + } + }) + } + onClose?.() + } + + const getSubmitButtonText = () => { + if (isCloneMode) { + return SubmitBtnText.CloneDatabase + } + if (editMode) { + return SubmitBtnText.EditDatabase + } + return SubmitBtnText.AddDatabase + } + + const handlePostHostName = (content: string): boolean => ( + autoFillFormDetails(content, formFields, setFormFields, InstanceType.Standalone) + ) + + return ( +
+ +
+ ) +} + +export default ManualConnectionWrapper diff --git a/redisinsight/ui/src/pages/home/components/manual-connection/index.ts b/redisinsight/ui/src/pages/home/components/manual-connection/index.ts new file mode 100644 index 0000000000..28d4f3af8c --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/manual-connection/index.ts @@ -0,0 +1,3 @@ +import ManualConnectionWrapper from './ManualConnectionWrapper' + +export default ManualConnectionWrapper diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.tsx similarity index 53% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx rename to redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.tsx index 27c019c9cf..89f1c9fb93 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx +++ b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.tsx @@ -9,138 +9,77 @@ import { keys, } from '@elastic/eui' import { FormikErrors, useFormik } from 'formik' -import { isEmpty, pick, toString } from 'lodash' +import { isEmpty, pick } from 'lodash' import React, { useEffect, useRef, useState } from 'react' import ReactDOM from 'react-dom' import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router' -import { PageNames, Pages } from 'uiSrc/constants' import validationErrors from 'uiSrc/constants/validationErrors' -import DatabaseAlias from 'uiSrc/pages/home/components/DatabaseAlias' +import DatabaseAlias from 'uiSrc/pages/home/components/database-alias' import { useResizableFormField } from 'uiSrc/services' -import { appContextSelector, setAppContextInitialState } from 'uiSrc/slices/app/context' -import { resetKeys } from 'uiSrc/slices/browser/keys' -import { - changeInstanceAliasAction, - checkConnectToInstanceAction, - resetInstanceUpdateAction, - setConnectedInstanceId, -} from 'uiSrc/slices/instances/instances' -import { ConnectionType, InstanceType, } from 'uiSrc/slices/interfaces' -import { getRedisModulesSummary, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { Nullable, getDiffKeysOfObjectValues, isRediStack } from 'uiSrc/utils' +import { resetInstanceUpdateAction } from 'uiSrc/slices/instances/instances' +import { ConnectionType } from 'uiSrc/slices/interfaces' +import { isRediStack } from 'uiSrc/utils' import { BuildType } from 'uiSrc/constants/env' import { appRedirectionSelector } from 'uiSrc/slices/app/url-handling' import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' import { - ADD_NEW_CA_CERT, - NO_CA_CERT, - ADD_NEW, fieldDisplayNames, - SshPassType, - DEFAULT_TIMEOUT, - NONE, -} from './constants' - -import { DbConnectionInfo, ISubmitButton } from './interfaces' + SubmitBtnText, +} from 'uiSrc/pages/home/constants' +import { getFormErrors, getSubmitButtonContent } from 'uiSrc/pages/home/utils' +import { DbConnectionInfo, ISubmitButton } from 'uiSrc/pages/home/interfaces' import { DbIndex, DbInfo, - MessageSentinel, MessageStandalone, TlsDetails, DatabaseForm, - DbCompressor -} from './form-components' + DbCompressor, + SSHDetails, +} from 'uiSrc/pages/home/components/form' import { DbInfoSentinel, PrimaryGroupSentinel, SentinelHostPort, SentinelMasterDatabase, -} from './form-components/sentinel' -import SSHDetails from './form-components/SSHDetails' -import { LoadingDatabaseText, SubmitBtnText, TitleDatabaseText, } from '../InstanceFormWrapper' +} from 'uiSrc/pages/home/components/form/sentinel' +import { caCertsSelector } from 'uiSrc/slices/instances/caCerts' +import { clientCertsSelector } from 'uiSrc/slices/instances/clientCerts' export interface Props { width: number - isResizablePanel?: boolean formFields: DbConnectionInfo submitButtonText?: SubmitBtnText - titleText?: TitleDatabaseText loading: boolean buildType?: BuildType - instanceType: InstanceType - loadingMsg: LoadingDatabaseText isEditMode: boolean isCloneMode: boolean setIsCloneMode: (value: boolean) => void - initialValues: DbConnectionInfo onSubmit: (values: DbConnectionInfo) => void onTestConnection: (values: DbConnectionInfo) => void - updateEditingName: (name: string) => void onHostNamePaste: (content: string) => boolean onClose?: () => void onAliasEdited?: (value: string) => void - setErrorMsgRef?: (database: HTMLDivElement | null) => void - urlHandlingAction?: Nullable } -const getInitFieldsDisplayNames = ({ host, port, name, instanceType }: any) => { - if (!host || !port) { - if (!name && instanceType !== InstanceType.Sentinel) { - return pick(fieldDisplayNames, ['host', 'port', 'name']) - } - return pick(fieldDisplayNames, ['host', 'port']) +const getInitFieldsDisplayNames = ({ host, port, name }: any) => { + if (!host || !port || !name) { + return pick(fieldDisplayNames, ['host', 'port', 'name']) } return {} } -const getDefaultHost = () => '127.0.0.1' -const getDefaultPort = (instanceType: InstanceType) => (instanceType === InstanceType.Sentinel ? '26379' : '6379') - -const AddStandaloneForm = (props: Props) => { +const ManualConnectionForm = (props: Props) => { const { - formFields: { - id, - host, - name, - port, - tls, - db = null, - compressor = NONE, - nameFromProvider, - sentinelMaster, - connectionType, - nodes = null, - tlsClientAuthRequired, - certificates, - selectedTlsClientCertId = '', - verifyServerTlsCert, - caCertificates, - selectedCaCertName, - username, - password, - timeout, - modules, - sentinelMasterPassword, - sentinelMasterUsername, - servername, - provider, - ssh, - sshPassType = SshPassType.Password, - sshOptions, - version, - }, - initialValues: initialValuesProp, + formFields, width, onClose, onSubmit, onTestConnection, onHostNamePaste, submitButtonText, - instanceType, buildType, loading, isEditMode, @@ -149,60 +88,29 @@ const AddStandaloneForm = (props: Props) => { onAliasEdited, } = props - const { contextInstanceId, lastPage } = useSelector(appContextSelector) - const { action } = useSelector(appRedirectionSelector) - - const prepareInitialValues = () => ({ - host: host ?? getDefaultHost(), - port: port ? port.toString() : getDefaultPort(instanceType), - timeout: timeout ? timeout.toString() : toString(DEFAULT_TIMEOUT / 1_000), - name: name ?? `${getDefaultHost()}:${getDefaultPort(instanceType)}`, - username, - password, - tls, - db, - compressor, + const { + id, + host, + name, + port, + db = null, + nameFromProvider, + sentinelMaster, + connectionType, + nodes = null, modules, - showDb: !!db, - showCompressor: compressor !== NONE, - sni: !!servername, - servername, - newCaCert: '', - newCaCertName: '', - selectedCaCertName, - tlsClientAuthRequired, - verifyServerTlsCert, - newTlsCertPairName: '', - selectedTlsClientCertId, - newTlsClientCert: '', - newTlsClientKey: '', - sentinelMasterName: sentinelMaster?.name || '', - sentinelMasterUsername, - sentinelMasterPassword, - ssh, - sshPassType, - sshHost: sshOptions?.host ?? '', - sshPort: sshOptions?.port ?? 22, - sshUsername: sshOptions?.username ?? '', - sshPassword: sshOptions?.password ?? '', - sshPrivateKey: sshOptions?.privateKey ?? '', - sshPassphrase: sshOptions?.passphrase ?? '' - }) + provider, + version, + } = formFields - const [initialValues, setInitialValues] = useState(prepareInitialValues()) + const { action } = useSelector(appRedirectionSelector) + const { data: caCertificates } = useSelector(caCertsSelector) + const { data: certificates } = useSelector(clientCertsSelector) const [errors, setErrors] = useState>( - getInitFieldsDisplayNames({ host, port, name, instanceType }) + getInitFieldsDisplayNames({ host, port, name }) ) - useEffect(() => { - const values = prepareInitialValues() - - setInitialValues(values) - formik.setValues(values) - }, [initialValuesProp, isCloneMode]) - - const history = useHistory() const dispatch = useDispatch() const formRef = useRef(null) @@ -211,84 +119,14 @@ const AddStandaloneForm = (props: Props) => { const isFromCloud = action === UrlHandlingActions.Connect const validate = (values: DbConnectionInfo) => { - const errs: FormikErrors = {} - - if (!values.host) { - errs.host = fieldDisplayNames.host - } - if (!values.port) { - errs.port = fieldDisplayNames.port - } - - if (!values.name && instanceType !== InstanceType.Sentinel) { - errs.name = fieldDisplayNames.name - } - - if ( - values.tls - && values.verifyServerTlsCert - && values.selectedCaCertName === NO_CA_CERT - ) { - errs.selectedCaCertName = fieldDisplayNames.selectedCaCertName - } - - if ( - values.tls - && values.selectedCaCertName === ADD_NEW_CA_CERT - && values.newCaCertName === '' - ) { - errs.newCaCertName = fieldDisplayNames.newCaCertName - } - - if ( - values.tls - && values.selectedCaCertName === ADD_NEW_CA_CERT - && values.newCaCert === '' - ) { - errs.newCaCert = fieldDisplayNames.newCaCert - } - - if ( - values.tls - && values.sni - && values.servername === '' - ) { - errs.servername = fieldDisplayNames.servername - } - - if ( - values.tls - && values.tlsClientAuthRequired - && values.selectedTlsClientCertId === ADD_NEW - ) { - if (values.newTlsCertPairName === '') { - errs.newTlsCertPairName = fieldDisplayNames.newTlsCertPairName - } - if (values.newTlsClientCert === '') { - errs.newTlsClientCert = fieldDisplayNames.newTlsClientCert - } - if (values.newTlsClientKey === '') { - errs.newTlsClientKey = fieldDisplayNames.newTlsClientKey - } - } + const errs = getFormErrors(values) if (isCloneMode && connectionType === ConnectionType.Sentinel && !values.sentinelMasterName) { errs.sentinelMasterName = fieldDisplayNames.sentinelMasterName } - if (values.ssh) { - if (!values.sshHost) { - errs.sshHost = fieldDisplayNames.sshHost - } - if (!values.sshPort) { - errs.sshPort = fieldDisplayNames.sshPort - } - if (!values.sshUsername) { - errs.sshUsername = fieldDisplayNames.sshUsername - } - if (values.sshPassType === SshPassType.PrivateKey && !values.sshPrivateKey) { - errs.sshPrivateKey = fieldDisplayNames.sshPrivateKey - } + if (!values.name) { + errs.name = fieldDisplayNames.name } setErrors(errs) @@ -296,19 +134,10 @@ const AddStandaloneForm = (props: Props) => { } const formik = useFormik({ - initialValues, + initialValues: formFields, validate, enableReinitialize: true, onSubmit: (values: any) => { - if (isCloneMode) { - const diffKeys = getDiffKeysOfObjectValues(formik.initialValues, values) - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CONFIRMED, - eventData: { - fieldsModified: diffKeys - } - }) - } onSubmit(values) }, }) @@ -326,7 +155,7 @@ const AddStandaloneForm = (props: Props) => { } useEffect(() => - // componentWillUnmount + // componentWillUnmount () => { if (isEditMode) { dispatch(resetInstanceUpdateAction()) @@ -334,88 +163,14 @@ const AddStandaloneForm = (props: Props) => { }, []) - const handleCheckConnectToInstance = () => { - const modulesSummary = getRedisModulesSummary(modules) - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_OPEN_DATABASE_BUTTON_CLICKED, - eventData: { - databaseId: id, - provider, - ...modulesSummary, - } - }) - dispatch(checkConnectToInstanceAction(id, connectToInstance)) - } - - const handleCloneDatabase = () => { - setIsCloneMode(true) - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_REQUESTED, - eventData: { - databaseId: id - } - }) - } - - const handleBackCloneDatabase = () => { - setIsCloneMode(false) - sendEventTelemetry({ - event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED, - eventData: { - databaseId: id - } - }) - } + useEffect(() => { + formik.resetForm() + }, [isCloneMode]) const handleTestConnectionDatabase = () => { onTestConnection(formik.values) } - const handleChangeDatabaseAlias = ( - value: string, - onSuccess?: () => void, - onFail?: () => void - ) => { - dispatch(changeInstanceAliasAction( - id, - value, - () => { - onAliasEdited?.(value) - onSuccess?.() - }, - onFail - )) - } - - const connectToInstance = () => { - if (contextInstanceId && contextInstanceId !== id) { - dispatch(resetKeys()) - dispatch(setAppContextInitialState()) - } - dispatch(setConnectedInstanceId(id ?? '')) - - if (lastPage === PageNames.workbench && contextInstanceId === id) { - history.push(Pages.workbench(id)) - return - } - history.push(Pages.browser(id)) - } - - const getSubmitButtonContent = (submitIsDisabled?: boolean) => { - const maxErrorsCount = 5 - const errorsArr = Object.values(errors).map((err) => [ - err, -
, - ]) - - if (errorsArr.length > maxErrorsCount) { - errorsArr.splice(maxErrorsCount, errorsArr.length, ['...']) - } - return submitIsDisabled ? ( - {errorsArr} - ) : null - } - const SubmitButton = ({ text = '', onClick, @@ -429,7 +184,7 @@ const AddStandaloneForm = (props: Props) => { ? validationErrors.REQUIRED_TITLE(Object.keys(errors).length) : null } - content={getSubmitButtonContent(submitIsDisabled)} + content={getSubmitButtonContent(errors, submitIsDisabled)} > { responsive={false} > - {instanceType !== InstanceType.Sentinel && ( - + - - Test Connection - - - )} + Test Connection + +
@@ -520,26 +273,21 @@ const AddStandaloneForm = (props: Props) => { alias={name} database={db} isLoading={loading} - onOpen={handleCheckConnectToInstance} - onClone={handleCloneDatabase} - onCloneBack={handleBackCloneDatabase} - onApplyChanges={handleChangeDatabaseAlias} + id={id} + provider={provider} + modules={modules} + setIsCloneMode={setIsCloneMode} + onAliasEdited={onAliasEdited} />
)}
- {!isEditMode && instanceType === InstanceType.Standalone && !isFromCloud && ( + {!isEditMode && !isFromCloud && ( <>
)} - {!isEditMode && instanceType === InstanceType.Sentinel && ( - <> - -
- - )} {!isEditMode && !isFromCloud && ( { formik={formik} flexItemClassName={flexItemClassName} flexGroupClassName={flexGroupClassName} - isCloneMode={isCloneMode} - isEditMode={isEditMode} - connectionType={connectionType} - instanceType={instanceType} onHostNamePaste={onHostNamePaste} + showFields={{ host: true, alias: true, port: true, timeout: true }} + /> + + - {instanceType !== InstanceType.Sentinel && ( - - )} - {instanceType !== InstanceType.Sentinel && ( - - )} { certificates={certificates} caCertificates={caCertificates} /> - {instanceType !== InstanceType.Sentinel && buildType !== BuildType.RedisStack && ( + {buildType !== BuildType.RedisStack && ( { formik={formik} flexItemClassName={flexItemClassName} flexGroupClassName={flexGroupClassName} - isCloneMode={isCloneMode} - isEditMode={isEditMode} - isFromCloud={isFromCloud} - connectionType={connectionType} - instanceType={instanceType} + showFields={{ + alias: !isEditMode || isCloneMode, + host: (!isEditMode || isCloneMode) && !isFromCloud, + port: !isFromCloud, + timeout: true, + }} + autoFocus={!isCloneMode && isEditMode} onHostNamePaste={onHostNamePaste} /> {isCloneMode && ( @@ -690,14 +433,10 @@ const AddStandaloneForm = (props: Props) => { formik={formik} flexItemClassName={flexItemClassName} flexGroupClassName={flexGroupClassName} - isCloneMode={isCloneMode} - isEditMode={isEditMode} - connectionType={connectionType} - instanceType={instanceType} + showFields={{ host: false, port: true, alias: false, timeout: false }} onHostNamePaste={onHostNamePaste} /> - { formik={formik} flexItemClassName={flexItemClassName} flexGroupClassName={flexGroupClassName} - isCloneMode={isCloneMode} - isEditMode={isEditMode} - connectionType={connectionType} - instanceType={instanceType} + showFields={{ host: true, port: true, alias: false, timeout: false }} onHostNamePaste={onHostNamePaste} /> @@ -780,4 +516,4 @@ const AddStandaloneForm = (props: Props) => { ) } -export default AddStandaloneForm +export default ManualConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionFrom.spec.tsx similarity index 90% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx rename to redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionFrom.spec.tsx index cbd472a542..e003c23422 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionFrom.spec.tsx @@ -1,13 +1,14 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils' -import { ConnectionType, InstanceType } from 'uiSrc/slices/interfaces' +import { ConnectionType } from 'uiSrc/slices/interfaces' import { BuildType } from 'uiSrc/constants/env' import { appRedirectionSelector } from 'uiSrc/slices/app/url-handling' import { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling' -import InstanceForm, { Props } from './InstanceForm' -import { ADD_NEW_CA_CERT, SshPassType } from './constants' -import { DbConnectionInfo } from './interfaces' +import { ADD_NEW_CA_CERT, SshPassType } from 'uiSrc/pages/home/constants' +import { DbConnectionInfo } from 'uiSrc/pages/home/interfaces' + +import ManualConnectionForm, { Props } from './ManualConnectionForm' const BTN_SUBMIT = 'btn-submit' const NEW_CA_CERT = 'new-ca-cert' @@ -23,8 +24,6 @@ const formFields = { host: 'localhost', port: '6379', name: 'lala', - caCertificates: [], - certificates: [], } jest.mock('uiSrc/slices/instances/instances', () => ({ @@ -43,7 +42,7 @@ describe('InstanceForm', () => { it('should render', () => { expect( render( - + ) ).toBeTruthy() }) @@ -51,7 +50,7 @@ describe('InstanceForm', () => { it('should render with ConnectionType.Sentinel', () => { expect( render( - { it('should render with ConnectionType.Cluster', () => { expect( render( - { it('should render tooltip with nodes', () => { expect( render( - { it('should render DatabaseForm', () => { expect( render( - { render(
- { const handleSubmit = jest.fn() render(
- { render(
- { const handleTestConnection = jest.fn() render(
- { }) expect(handleTestConnection).toBeCalledWith( expect.objectContaining({ - showDb: true, + showDb: ['on'], }) ) await act(() => { @@ -272,7 +271,7 @@ describe('InstanceForm', () => { expect(handleSubmit).toBeCalledWith( expect.objectContaining({ - showDb: true, + showDb: ['on'], }) ) }) @@ -282,7 +281,7 @@ describe('InstanceForm', () => { const handleTestConnection = jest.fn() render(
- { }) expect(handleTestConnection).toBeCalledWith( expect.objectContaining({ - showDb: true, + showDb: ['on'], db: '12' }) ) @@ -321,7 +320,7 @@ describe('InstanceForm', () => { expect(handleSubmit).toBeCalledWith( expect.objectContaining({ - showDb: true, + showDb: ['on'], db: '12' }) ) @@ -332,7 +331,7 @@ describe('InstanceForm', () => { const handleTestConnection = jest.fn() render(
- { }) expect(handleTestConnection).toBeCalledWith( expect.objectContaining({ - sni: true, + sni: ['on'], servername: formFields.host }) ) @@ -366,7 +365,7 @@ describe('InstanceForm', () => { expect(handleSubmit).toBeCalledWith( expect.objectContaining({ - sni: true, + sni: ['on'], servername: formFields.host }) ) @@ -377,7 +376,7 @@ describe('InstanceForm', () => { const handleTestConnection = jest.fn() render(
- { }) expect(handleTestConnection).toBeCalledWith( expect.objectContaining({ - sni: true, + sni: ['on'], servername: '12' }) ) @@ -417,7 +416,7 @@ describe('InstanceForm', () => { expect(handleSubmit).toBeCalledWith( expect.objectContaining({ - sni: true, + sni: ['on'], servername: '12' }) ) @@ -428,7 +427,7 @@ describe('InstanceForm', () => { const handleTestConnection = jest.fn() render(
- { const handleTestConnection = jest.fn() const { queryByText } = render(
- { const handleTestConnection = jest.fn() render(
- { const handleTestConnection = jest.fn() render(
- { const handleSubmit = jest.fn() const { container } = render(
- { it('should render clone mode btn', () => { render( - { describe('should render proper fields with Clone mode', () => { it('should render proper fields for standalone db', () => { render( - { it('should render proper fields for sentinel db', () => { render( - { it('should render selected logical database with proper db index', () => { render( - @@ -779,7 +779,7 @@ describe('InstanceForm', () => { it('should render proper database alias', () => { render( - { expect(screen.getByTestId('db-alias')).toHaveTextContent('Clone ') }) - it('should render proper default values for standalone', () => { - render( - - ) - expect(screen.getByTestId('host')).toHaveValue('127.0.0.1') - expect(screen.getByTestId('port')).toHaveValue('6379') - expect(screen.getByTestId('name')).toHaveValue('127.0.0.1:6379') - }) - - it('should render proper default values for sentinel', () => { - render( - - ) - expect(screen.getByTestId('host')).toHaveValue('127.0.0.1') - expect(screen.getByTestId('port')).toHaveValue('26379') - }) + // it('should render proper default values for standalone', () => { + // render( + // + // ) + // expect(screen.getByTestId('host')).toHaveValue('127.0.0.1') + // expect(screen.getByTestId('port')).toHaveValue('6379') + // expect(screen.getByTestId('name')).toHaveValue('127.0.0.1:6379') + // }) }) it('should change Use SSH checkbox', async () => { const handleSubmit = jest.fn() render(
- {
) - fireEvent.click(screen.getByTestId('use-ssh')) + act(() => { + fireEvent.click(screen.getByTestId('use-ssh')) + }) expect(screen.getByTestId('use-ssh')).toBeChecked() }) @@ -841,7 +831,7 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() render(
- { const handleSubmit = jest.fn() render(
- {
) - fireEvent.click(screen.getByTestId('use-ssh')) + act(() => { + fireEvent.click(screen.getByTestId('use-ssh')) + }) expect(screen.getByTestId('sshHost')).toBeInTheDocument() expect(screen.getByTestId('sshPort')).toBeInTheDocument() - expect(screen.getByTestId('sshPort')).toHaveValue('22') expect(screen.getByTestId('sshPassword')).toBeInTheDocument() expect(screen.queryByTestId('sshPrivateKey')).not.toBeInTheDocument() expect(screen.queryByTestId('sshPassphrase')).not.toBeInTheDocument() @@ -888,7 +880,7 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() const { container } = render(
- { expect(screen.getByTestId('sshHost')).toBeInTheDocument() expect(screen.getByTestId('sshPort')).toBeInTheDocument() - expect(screen.getByTestId('sshPort')).toHaveValue('22') expect(screen.queryByTestId('sshPassword')).not.toBeInTheDocument() expect(screen.getByTestId('sshPrivateKey')).toBeInTheDocument() expect(screen.getByTestId('sshPassphrase')).toBeInTheDocument() @@ -921,11 +912,12 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() render(
- @@ -956,6 +948,15 @@ describe('InstanceForm', () => { ) }) + expect(screen.getByTestId(BTN_SUBMIT)).toBeDisabled() + + await act(() => { + fireEvent.change( + screen.getByTestId('sshPort'), + { target: { value: '22' } } + ) + }) + expect(screen.getByTestId(BTN_SUBMIT)).not.toBeDisabled() }) @@ -963,11 +964,12 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() const { container } = render(
- @@ -990,6 +992,10 @@ describe('InstanceForm', () => { screen.getByTestId('sshHost'), { target: { value: 'localhost' } } ) + fireEvent.change( + screen.getByTestId('sshPort'), + { target: { value: '22' } } + ) fireEvent.change( screen.getByTestId('sshUsername'), { target: { value: 'username' } } @@ -1012,11 +1018,12 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() render(
- @@ -1067,7 +1074,7 @@ describe('InstanceForm', () => { const handleSubmit = jest.fn() const { container } = render(
- { it('should render password input with 10_000 length limit', () => { render( - @@ -1140,14 +1147,14 @@ describe('InstanceForm', () => { it('should render security fields with proper attributes', () => { render( - @@ -1167,13 +1174,13 @@ describe('InstanceForm', () => { it('should render ssh password with proper attributes', () => { render( - @@ -1189,9 +1196,14 @@ describe('InstanceForm', () => { it('should render ssh password input with 10_000 length limit', () => { render( - ) @@ -1201,7 +1213,7 @@ describe('InstanceForm', () => { describe('timeout', () => { it('should render timeout input with 7 length limit and 1_000_000 value', () => { render( - @@ -1220,7 +1232,7 @@ describe('InstanceForm', () => { it('should put only numbers', () => { render( - @@ -1242,7 +1254,7 @@ describe('InstanceForm', () => { })) const { queryByTestId } = render( - @@ -1255,4 +1267,22 @@ describe('InstanceForm', () => { expect(queryByTestId('db-info-host')).toBeInTheDocument() }) }) + + it('should call submit on press Enter', async () => { + const handleSubmit = jest.fn() + render( +
+ +
+ ) + + await act(() => { + fireEvent.keyDown(screen.getByTestId('form'), { key: 'Enter', code: 13, charCode: 13 }) + }) + expect(handleSubmit).toBeCalled() + }) }) diff --git a/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/index.ts b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/index.ts new file mode 100644 index 0000000000..600d1f3024 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/index.ts @@ -0,0 +1,3 @@ +import ManualConnectionForm from './ManualConnectionForm' + +export default ManualConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.spec.tsx b/redisinsight/ui/src/pages/home/components/search-databases-list/SearchDatabasesList.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.spec.tsx rename to redisinsight/ui/src/pages/home/components/search-databases-list/SearchDatabasesList.spec.tsx diff --git a/redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.tsx b/redisinsight/ui/src/pages/home/components/search-databases-list/SearchDatabasesList.tsx similarity index 100% rename from redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.tsx rename to redisinsight/ui/src/pages/home/components/search-databases-list/SearchDatabasesList.tsx diff --git a/redisinsight/ui/src/pages/home/components/SearchDatabasesList/index.ts b/redisinsight/ui/src/pages/home/components/search-databases-list/index.ts similarity index 100% rename from redisinsight/ui/src/pages/home/components/SearchDatabasesList/index.ts rename to redisinsight/ui/src/pages/home/components/search-databases-list/index.ts diff --git a/redisinsight/ui/src/pages/home/components/SearchDatabasesList/styles.module.scss b/redisinsight/ui/src/pages/home/components/search-databases-list/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/SearchDatabasesList/styles.module.scss rename to redisinsight/ui/src/pages/home/components/search-databases-list/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.spec.tsx b/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.spec.tsx new file mode 100644 index 0000000000..b37d75b206 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.spec.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import SentinelConnectionForm, { Props as SentinelConnectionFormProps } from + 'uiSrc/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import SentinelConnectionWrapper, { + Props, +} from './SentinelConnectionWrapper' + +const mockedProps = mock() + +jest.mock('./sentinel-connection-form/SentinelConnectionForm', () => ({ + __esModule: true, + namedExport: jest.fn(), + default: jest.fn(), +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +const mockSentinelConnectionForm = (props: SentinelConnectionFormProps) => ( +
+ + + +
+) + +describe('SentinelConnectionWrapper', () => { + beforeAll(() => { + SentinelConnectionForm.mockImplementation(mockSentinelConnectionForm) + }) + it('should render', () => { + expect( + render() + ).toBeTruthy() + }) + + it('should call onHostNamePaste', () => { + const component = render() + fireEvent.click(screen.getByTestId('onHostNamePaste-btn')) + expect(component).toBeTruthy() + }) + + it('should call onClose', () => { + const onClose = jest.fn() + render() + fireEvent.click(screen.getByTestId('onClose-btn')) + expect(onClose).toBeCalled() + }) + + it('Should call proper telemetry event', () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render() + + fireEvent.click(screen.getByTestId('onSubmit-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUBMITTED, + }) + + sendEventTelemetry.mockRestore() + }) +}) diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.tsx b/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.tsx new file mode 100644 index 0000000000..a462959230 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router' + +import { fetchMastersSentinelAction, sentinelSelector, } from 'uiSrc/slices/instances/sentinel' +import { removeEmpty } from 'uiSrc/utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { caCertsSelector, fetchCaCerts } from 'uiSrc/slices/instances/caCerts' +import { Pages } from 'uiSrc/constants' +import { clientCertsSelector, fetchClientCerts, } from 'uiSrc/slices/instances/clientCerts' + +import { DbConnectionInfo } from 'uiSrc/pages/home/interfaces' +import { applyTlSDatabase, autoFillFormDetails, getTlsSettings } from 'uiSrc/pages/home/utils' +import { ADD_NEW, NO_CA_CERT } from 'uiSrc/pages/home/constants' +import { InstanceType } from 'uiSrc/slices/interfaces' +import SentinelConnectionForm from './sentinel-connection-form' + +export interface Props { + width: number + onClose?: () => void +} +const DEFAULT_SENTINEL_HOST = '127.0.0.1' +const DEFAULT_SENTINEL_PORT = '26379' + +const INITIAL_VALUES = { + host: DEFAULT_SENTINEL_HOST, + port: DEFAULT_SENTINEL_PORT, + username: '', + password: '', + tls: false, + tlsClientAuthRequired: false, + selectedTlsClientCertId: ADD_NEW, + verifyServerTlsCert: false, + selectedCaCertName: NO_CA_CERT, + +} + +const SentinelConnectionWrapper = (props: Props) => { + const { + width, + onClose, + } = props + const [initialValues, setInitialValues] = useState(INITIAL_VALUES) + + const { loading } = useSelector(sentinelSelector) + const { data: caCertificates } = useSelector(caCertsSelector) + const { data: certificates } = useSelector(clientCertsSelector) + + const history = useHistory() + const dispatch = useDispatch() + + useEffect(() => { + dispatch(fetchCaCerts()) + dispatch(fetchClientCerts()) + }, []) + + const onMastersSentinelFetched = () => { + history.push(Pages.sentinelDatabases) + } + + const handleSubmitDatabase = (payload: any) => { + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUBMITTED + }) + + dispatch(fetchMastersSentinelAction(payload, onMastersSentinelFetched)) + } + + const addDatabase = (tlsSettings: any, values: DbConnectionInfo) => { + const { + host, + port, + username, + password, + } = values + const database: any = { + host, + port: +port, + username, + password, + } + + // add tls for database + applyTlSDatabase(database, tlsSettings) + handleSubmitDatabase(removeEmpty(database)) + } + + const handleConnectionFormSubmit = (values: DbConnectionInfo) => { + const tlsSettings = getTlsSettings(values) + + addDatabase(tlsSettings, values) + } + + const handlePostHostName = (content: string): boolean => ( + autoFillFormDetails(content, initialValues, setInitialValues, InstanceType.Sentinel) + ) + + return ( +
+ +
+ ) +} + +export default SentinelConnectionWrapper diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/index.ts b/redisinsight/ui/src/pages/home/components/sentinel-connection/index.ts new file mode 100644 index 0000000000..0947d0226b --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/index.ts @@ -0,0 +1,3 @@ +import SentinelConnectionWrapper from './SentinelConnectionWrapper' + +export default SentinelConnectionWrapper diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.spec.tsx b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.spec.tsx new file mode 100644 index 0000000000..53958ab3d4 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.spec.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import SentinelConnectionForm, { Props } from './SentinelConnectionForm' + +const mockedProps = mock() + +const mockValues = { + host: 'host', + port: '123' +} + +describe('SentinelConnectionForm', () => { + it('should render', () => { + expect( + render() + ).toBeTruthy() + }) + + it('should call submit form on press Enter', async () => { + const mockSubmit = jest.fn() + render() + + await act(() => { + fireEvent.keyDown(screen.getByTestId('form'), { key: 'Enter', code: 13, charCode: 13 }) + }) + + expect(mockSubmit).toBeCalled() + }) + + it('should render Footer', async () => { + render( +
+ +
+ ) + + expect(screen.getByTestId('btn-submit')).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.tsx b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.tsx new file mode 100644 index 0000000000..bf4dff6350 --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.tsx @@ -0,0 +1,192 @@ +import { + EuiButton, + EuiForm, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + keys, +} from '@elastic/eui' +import { FormikErrors, useFormik } from 'formik' +import { isEmpty, pick } from 'lodash' +import React, { useRef, useState } from 'react' +import ReactDOM from 'react-dom' + +import validationErrors from 'uiSrc/constants/validationErrors' +import { useResizableFormField } from 'uiSrc/services' +import { + fieldDisplayNames, +} from 'uiSrc/pages/home/constants' +import { getFormErrors, getSubmitButtonContent } from 'uiSrc/pages/home/utils' +import { DbConnectionInfo, ISubmitButton, IPasswordType } from 'uiSrc/pages/home/interfaces' +import { + MessageSentinel, + TlsDetails, + DatabaseForm, +} from 'uiSrc/pages/home/components/form' + +export interface Props { + width: number + loading: boolean + initialValues: DbConnectionInfo + certificates: { id: string; name: string }[], + caCertificates: { id: string; name: string }[], + onSubmit: (values: DbConnectionInfo) => void + onHostNamePaste: (content: string) => boolean + onClose?: () => void +} + +const getInitFieldsDisplayNames = ({ host, port }: any) => { + if (!host || !port) { + return pick(fieldDisplayNames, ['host', 'port']) + } + return {} +} + +const SentinelConnectionForm = (props: Props) => { + const { + initialValues = {}, + width, + onClose, + onSubmit, + onHostNamePaste, + loading, + certificates, + caCertificates, + } = props + + const [errors, setErrors] = useState>( + getInitFieldsDisplayNames(initialValues) + ) + + const formRef = useRef(null) + + const submitIsDisable = () => !isEmpty(errors) + + const validate = (values: DbConnectionInfo) => { + const errs = getFormErrors(values) + setErrors(errs) + return errs + } + + const formik = useFormik({ + initialValues, + validate, + enableReinitialize: true, + onSubmit: (values: any) => { + onSubmit(values) + }, + }) + + const [flexGroupClassName, flexItemClassName] = useResizableFormField( + formRef, + width + ) + + const onKeyDown = (event: React.KeyboardEvent) => { + if (event.key === keys.ENTER && !submitIsDisable()) { + // event. + formik.submitForm() + } + } + + const SubmitButton = ({ + onClick, + submitIsDisabled, + }: ISubmitButton) => ( + + + Discover Database + + + ) + + const Footer = () => { + const footerEl = document.getElementById('footerDatabaseForm') + + if (footerEl) { + return ReactDOM.createPortal( + + + + + {onClose && ( + + Cancel + + )} + + + + , + footerEl + ) + } + return null + } + + return ( +
+
+ +
+ + + + +
+
+
+ ) +} + +export default SentinelConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/index.ts b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/index.ts new file mode 100644 index 0000000000..e598e90aae --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/index.ts @@ -0,0 +1,3 @@ +import SentinelConnectionForm from './SentinelConnectionForm' + +export default SentinelConnectionForm diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/styles.module.scss b/redisinsight/ui/src/pages/home/components/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/styles.module.scss rename to redisinsight/ui/src/pages/home/components/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.spec.tsx b/redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.spec.tsx similarity index 97% rename from redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.spec.tsx rename to redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.spec.tsx index b5d92c3fbc..3f8301c8c8 100644 --- a/redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.spec.tsx @@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash' import { render, screen, fireEvent, mockedStore, cleanup } from 'uiSrc/utils/test-utils' import { contentSelector } from 'uiSrc/slices/content/create-redis-buttons' import { MOCKED_CREATE_REDIS_BTN_CONTENT } from 'uiSrc/mocks/content/content' -import { AddDbType } from 'uiSrc/pages/home/components/AddDatabases/AddDatabasesContainer' +import { AddDbType } from 'uiSrc/pages/home/constants' import { setSocialDialogState } from 'uiSrc/slices/oauth/cloud' import { OAuthSocialSource } from 'uiSrc/slices/interfaces' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' diff --git a/redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.tsx b/redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.tsx similarity index 96% rename from redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.tsx rename to redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.tsx index 3c05be266c..9d61200e3c 100644 --- a/redisinsight/ui/src/pages/home/components/WelcomeComponent/WelcomeComponent.tsx +++ b/redisinsight/ui/src/pages/home/components/welcome-component/WelcomeComponent.tsx @@ -10,14 +10,14 @@ import { ThemeContext } from 'uiSrc/contexts/themeContext' import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry' import darkLogo from 'uiSrc/assets/img/dark_logo.svg' import lightLogo from 'uiSrc/assets/img/light_logo.svg' -import { AddDbType } from 'uiSrc/pages/home/components/AddDatabases/AddDatabasesContainer' import { ReactComponent as CloudStars } from 'uiSrc/assets/img/oauth/stars.svg' import { ReactComponent as CloudIcon } from 'uiSrc/assets/img/oauth/cloud.svg' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' import { contentSelector } from 'uiSrc/slices/content/create-redis-buttons' import { getContentByFeature } from 'uiSrc/utils/content' -import { HELP_LINKS, IHelpGuide } from 'uiSrc/pages/home/constants/help-links' +import { AddDbType, HELP_LINKS, IHelpGuide } from 'uiSrc/pages/home/constants' + import { ContentCreateRedis } from 'uiSrc/slices/interfaces/content' import { FeatureFlagComponent, @@ -34,7 +34,7 @@ export interface Props { onAddInstance: (addDbType?: AddDbType) => void } -const Welcome = ({ onAddInstance }: Props) => { +const WelcomeComponent = ({ onAddInstance }: Props) => { const featureFlags = useSelector(appFeatureFlagsFeaturesSelector) const { loading, data } = useSelector(contentSelector) @@ -71,7 +71,7 @@ const Welcome = ({ onAddInstance }: Props) => { buttons: [ { title: 'Import Redis Cloud database connections', - description: 'Sign in to your Redis Enterprise Cloud account to discover and add databases', + description: 'Sign in to your Redis Cloud account to discover and add databases', iconType: CloudIcon, iconClassName: styles.cloudIcon, feature: FeatureFlags.cloudSso, @@ -293,4 +293,4 @@ const Welcome = ({ onAddInstance }: Props) => { ) } -export default Welcome +export default WelcomeComponent diff --git a/redisinsight/ui/src/pages/home/components/welcome-component/index.ts b/redisinsight/ui/src/pages/home/components/welcome-component/index.ts new file mode 100644 index 0000000000..b198b75e9b --- /dev/null +++ b/redisinsight/ui/src/pages/home/components/welcome-component/index.ts @@ -0,0 +1,3 @@ +import WelcomeComponent from './WelcomeComponent' + +export default WelcomeComponent diff --git a/redisinsight/ui/src/pages/home/components/WelcomeComponent/styles.module.scss b/redisinsight/ui/src/pages/home/components/welcome-component/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/home/components/WelcomeComponent/styles.module.scss rename to redisinsight/ui/src/pages/home/components/welcome-component/styles.module.scss diff --git a/redisinsight/ui/src/pages/home/constants/database.ts b/redisinsight/ui/src/pages/home/constants/database.ts new file mode 100644 index 0000000000..edd8e9585c --- /dev/null +++ b/redisinsight/ui/src/pages/home/constants/database.ts @@ -0,0 +1,4 @@ +export enum AddDbType { + manual, + auto, +} diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/constants.ts b/redisinsight/ui/src/pages/home/constants/form.ts similarity index 76% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/constants.ts rename to redisinsight/ui/src/pages/home/constants/form.ts index 057ce40220..5229a0a649 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/constants.ts +++ b/redisinsight/ui/src/pages/home/constants/form.ts @@ -2,6 +2,9 @@ export const ADD_NEW_CA_CERT = 'ADD_NEW_CA_CERT' export const NO_CA_CERT = 'NO_CA_CERT' export const ADD_NEW = 'ADD_NEW' export const NONE = 'NONE' +export const DEFAULT_HOST = '127.0.0.1' +export const DEFAULT_PORT = '6379' +export const DEFAULT_ALIAS = `${DEFAULT_HOST}:${DEFAULT_PORT}` export enum SshPassType { Password = 'password', @@ -29,3 +32,9 @@ export const fieldDisplayNames = { const DEFAULT_TIMEOUT_ENV = process.env.CONNECTIONS_TIMEOUT_DEFAULT || '30000' // 30 sec export const DEFAULT_TIMEOUT = parseInt(DEFAULT_TIMEOUT_ENV, 10) + +export enum SubmitBtnText { + AddDatabase = 'Add Redis Database', + EditDatabase = 'Apply Changes', + CloneDatabase = 'Clone Database' +} diff --git a/redisinsight/ui/src/pages/home/constants/index.ts b/redisinsight/ui/src/pages/home/constants/index.ts new file mode 100644 index 0000000000..59ec3920ec --- /dev/null +++ b/redisinsight/ui/src/pages/home/constants/index.ts @@ -0,0 +1,3 @@ +export * from './form' +export * from './help-links' +export * from './database' diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/interfaces.ts b/redisinsight/ui/src/pages/home/interfaces/form.ts similarity index 84% rename from redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/interfaces.ts rename to redisinsight/ui/src/pages/home/interfaces/form.ts index a52a19322a..909f19846e 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/interfaces.ts +++ b/redisinsight/ui/src/pages/home/interfaces/form.ts @@ -1,7 +1,8 @@ import { Instance } from 'uiSrc/slices/interfaces' -import { ADD_NEW_CA_CERT, NO_CA_CERT } from './constants' +import { ADD_NEW_CA_CERT, NO_CA_CERT } from 'uiSrc/pages/home/constants' export interface DbConnectionInfo extends Instance { + id?: string port: string tlsClientAuthRequired?: boolean certificates?: { id: number; name: string }[] @@ -26,8 +27,8 @@ export interface DbConnectionInfo extends Instance { sentinelMasterName?: string ssh?: boolean sshPassType?: string - sshHost: string - sshPort: string + sshHost?: string + sshPort?: string sshUsername?: string sshPassword?: string | true sshPrivateKey?: string | true @@ -39,3 +40,8 @@ export interface ISubmitButton { text?: string submitIsDisabled?: boolean } + +export enum IPasswordType { + Password = 'password', + Dual = 'dual', +} diff --git a/redisinsight/ui/src/pages/home/interfaces/index.ts b/redisinsight/ui/src/pages/home/interfaces/index.ts new file mode 100644 index 0000000000..54151771d5 --- /dev/null +++ b/redisinsight/ui/src/pages/home/interfaces/index.ts @@ -0,0 +1 @@ +export * from './form' diff --git a/redisinsight/ui/src/pages/home/utils/form.tsx b/redisinsight/ui/src/pages/home/utils/form.tsx new file mode 100644 index 0000000000..ddb493db44 --- /dev/null +++ b/redisinsight/ui/src/pages/home/utils/form.tsx @@ -0,0 +1,329 @@ +import { ConnectionString } from 'connection-string' +import { isUndefined, toString } from 'lodash' +import React from 'react' +import { FormikErrors } from 'formik' +import { REDIS_URI_SCHEMES } from 'uiSrc/constants' +import { InstanceType } from 'uiSrc/slices/interfaces' +import { + ADD_NEW, + ADD_NEW_CA_CERT, DEFAULT_ALIAS, + DEFAULT_HOST, DEFAULT_PORT, DEFAULT_TIMEOUT, + fieldDisplayNames, + NO_CA_CERT, NONE, + SshPassType +} from 'uiSrc/pages/home/constants' +import { DbConnectionInfo } from 'uiSrc/pages/home/interfaces' +import { Nullable } from 'uiSrc/utils' + +export const getTlsSettings = (values: DbConnectionInfo) => ({ + useTls: values.tls, + servername: (values.sni && values.servername) || undefined, + verifyServerCert: values.verifyServerTlsCert, + caCert: + !values.tls || values.selectedCaCertName === NO_CA_CERT + ? undefined + : values.selectedCaCertName === ADD_NEW_CA_CERT + ? { + new: { + name: values.newCaCertName, + certificate: values.newCaCert, + }, + } + : { + name: values.selectedCaCertName, + }, + clientAuth: values.tls && values.tlsClientAuthRequired, + clientCert: !values.tls + ? undefined + : typeof values.selectedTlsClientCertId === 'string' + && values.tlsClientAuthRequired + && values.selectedTlsClientCertId !== ADD_NEW + ? { id: values.selectedTlsClientCertId } + : values.selectedTlsClientCertId === ADD_NEW && values.tlsClientAuthRequired + ? { + new: { + name: values.newTlsCertPairName, + certificate: values.newTlsClientCert, + key: values.newTlsClientKey, + }, + } + : undefined, +}) + +export const applyTlSDatabase = (database: any, tlsSettings: any) => { + const { useTls, verifyServerCert, servername, caCert, clientAuth, clientCert } = tlsSettings + if (!useTls) return + + database.tls = useTls + database.tlsServername = servername + database.verifyServerCert = !!verifyServerCert + + if (!isUndefined(caCert?.new)) { + database.caCert = { + name: caCert?.new.name, + certificate: caCert?.new.certificate, + } + } + + if (!isUndefined(caCert?.name)) { + database.caCert = { id: caCert?.name } + } + + if (clientAuth) { + if (!isUndefined(clientCert.new)) { + database.clientCert = { + name: clientCert.new.name, + certificate: clientCert.new.certificate, + key: clientCert.new.key, + } + } + + if (!isUndefined(clientCert.id)) { + database.clientCert = { id: clientCert.id } + } + } +} + +export const applySSHDatabase = (database: any, values: DbConnectionInfo) => { + const { + ssh, + sshPassType, + sshHost, + sshPort, + sshPassword, + sshUsername, + sshPassphrase, + sshPrivateKey, + } = values + + if (ssh) { + database.ssh = true + database.sshOptions = { + host: sshHost, + port: +sshPort, + username: sshUsername, + } + + if (sshPassType === SshPassType.Password) { + database.sshOptions.password = sshPassword + database.sshOptions.passphrase = null + database.sshOptions.privateKey = null + } + + if (sshPassType === SshPassType.PrivateKey) { + database.sshOptions.password = null + database.sshOptions.passphrase = sshPassphrase + database.sshOptions.privateKey = sshPrivateKey + } + } +} + +export const getFormErrors = (values: DbConnectionInfo) => { + const errs: FormikErrors = {} + + if (!values.host) { + errs.host = fieldDisplayNames.host + } + if (!values.port) { + errs.port = fieldDisplayNames.port + } + + if ( + values.tls + && values.verifyServerTlsCert + && values.selectedCaCertName === NO_CA_CERT + ) { + errs.selectedCaCertName = fieldDisplayNames.selectedCaCertName + } + + if ( + values.tls + && values.selectedCaCertName === ADD_NEW_CA_CERT + && values.newCaCertName === '' + ) { + errs.newCaCertName = fieldDisplayNames.newCaCertName + } + + if ( + values.tls + && values.selectedCaCertName === ADD_NEW_CA_CERT + && values.newCaCert === '' + ) { + errs.newCaCert = fieldDisplayNames.newCaCert + } + + if ( + values.tls + && values.sni + && values.servername === '' + ) { + errs.servername = fieldDisplayNames.servername + } + + if ( + values.tls + && values.tlsClientAuthRequired + && values.selectedTlsClientCertId === ADD_NEW + ) { + if (values.newTlsCertPairName === '') { + errs.newTlsCertPairName = fieldDisplayNames.newTlsCertPairName + } + if (values.newTlsClientCert === '') { + errs.newTlsClientCert = fieldDisplayNames.newTlsClientCert + } + if (values.newTlsClientKey === '') { + errs.newTlsClientKey = fieldDisplayNames.newTlsClientKey + } + } + + if (values.ssh) { + if (!values.sshHost) { + errs.sshHost = fieldDisplayNames.sshHost + } + if (!values.sshPort) { + errs.sshPort = fieldDisplayNames.sshPort + } + if (!values.sshUsername) { + errs.sshUsername = fieldDisplayNames.sshUsername + } + if (values.sshPassType === SshPassType.PrivateKey && !values.sshPrivateKey) { + errs.sshPrivateKey = fieldDisplayNames.sshPrivateKey + } + } + + return errs +} + +export const autoFillFormDetails = ( + content: string, + initialValues: any, + setInitialValues: (data: any) => void, + instanceType: InstanceType +): boolean => { + try { + const details = new ConnectionString(content) + + /* If a protocol exists, it should be a redis protocol */ + if (details.protocol && !REDIS_URI_SCHEMES.includes(details.protocol)) return false + /* + * Auto fill logic: + * 1) If the port is parsed, we are sure that the user has indeed copied a connection string. + * '172.18.0.2:12000' => {host: '172,18.0.2', port: 12000} + * 'redis-12000.cluster.local:12000' => {host: 'redis-12000.cluster.local', port: 12000} + * 'lorem ipsum' => {host: undefined, port: undefined} + * 2) If the port is `undefined` but a redis URI scheme is present as protocol, we follow + * the "Scheme semantics" as mentioned in the official URI schemes. + * i) redis:// - https://www.iana.org/assignments/uri-schemes/prov/redis + * ii) rediss:// - https://www.iana.org/assignments/uri-schemes/prov/rediss + */ + if ( + details.port !== undefined + || REDIS_URI_SCHEMES.includes(details.protocol || '') + ) { + const getUpdatedInitialValues = () => { + switch (instanceType) { + case InstanceType.RedisEnterpriseCluster: { + return ({ + host: details.hostname || initialValues.host || 'localhost', + port: `${details.port || initialValues.port || 9443}`, + username: details.user || '', + password: details.password || '', + }) + } + + case InstanceType.Sentinel: { + return ({ + host: details.hostname || initialValues.host || 'localhost', + port: `${details.port || initialValues.port || 9443}`, + username: details.user || '', + password: details.password, + tls: details.protocol === 'rediss', + }) + } + + case InstanceType.Standalone: { + return ({ + name: details.host || initialValues.name || 'localhost:6379', + host: details.hostname || initialValues.host || 'localhost', + port: `${details.port || initialValues.port || 9443}`, + username: details.user || '', + password: details.password, + tls: details.protocol === 'rediss', + ssh: false, + sshPassType: SshPassType.Password + }) + } + default: { + return {} + } + } + } + setInitialValues(getFormValues(getUpdatedInitialValues())) + /* + * autofill was successfull so return true + */ + return true + } + } catch (err) { + /* The pasted content is not a connection URI so ignore. */ + return false + } + return false +} + +export const getSubmitButtonContent = (errors: FormikErrors, submitIsDisabled?: boolean) => { + const maxErrorsCount = 5 + const errorsArr = Object.values(errors).map((err) => [ + err, +
, + ]) + + if (errorsArr.length > maxErrorsCount) { + errorsArr.splice(maxErrorsCount, errorsArr.length, ['...']) + } + return submitIsDisabled ? ( + {errorsArr} + ) : null +} + +export const getFormValues = (instance?: Nullable>) => ({ + ...instance, + host: instance?.host ?? (instance ? '' : DEFAULT_HOST), + port: instance?.port?.toString() ?? (instance ? '' : DEFAULT_PORT), + timeout: instance?.timeout + ? toString(instance?.timeout / 1_000) + : toString(DEFAULT_TIMEOUT / 1_000), + name: instance?.name ?? (instance ? '' : DEFAULT_ALIAS), + username: instance?.username ?? '', + password: instance?.password ?? '', + tls: instance?.tls ?? false, + db: instance?.db, + compressor: instance?.compressor ?? NONE, + modules: instance?.modules, + showDb: !!instance?.db, + showCompressor: instance && instance.compressor && instance.compressor !== NONE, + sni: !!instance?.servername, + servername: instance?.servername, + newCaCert: '', + newCaCertName: '', + selectedCaCertName: instance?.caCert?.id ?? NO_CA_CERT, + tlsClientAuthRequired: instance?.clientCert?.id ?? false, + verifyServerTlsCert: instance?.verifyServerCert ?? false, + newTlsCertPairName: '', + selectedTlsClientCertId: instance?.clientCert?.id ?? ADD_NEW, + newTlsClientCert: '', + newTlsClientKey: '', + sentinelMasterName: instance?.sentinelMaster?.name || '', + sentinelMasterUsername: instance?.sentinelMaster?.username, + sentinelMasterPassword: instance?.sentinelMaster?.password, + ssh: instance?.ssh ?? false, + sshPassType: instance?.sshOptions + ? (instance.sshOptions.privateKey ? SshPassType.PrivateKey : SshPassType.Password) + : SshPassType.Password, + sshHost: instance?.sshOptions?.host ?? '', + sshPort: instance?.sshOptions?.port ?? 22, + sshUsername: instance?.sshOptions?.username ?? '', + sshPassword: instance?.sshOptions?.password ?? '', + sshPrivateKey: instance?.sshOptions?.privateKey ?? '', + sshPassphrase: instance?.sshOptions?.passphrase ?? '' +}) diff --git a/redisinsight/ui/src/pages/home/utils/index.ts b/redisinsight/ui/src/pages/home/utils/index.ts new file mode 100644 index 0000000000..54151771d5 --- /dev/null +++ b/redisinsight/ui/src/pages/home/utils/index.ts @@ -0,0 +1 @@ +export * from './form' diff --git a/redisinsight/ui/src/pages/instance/InstancePage.tsx b/redisinsight/ui/src/pages/instance/InstancePage.tsx index fd09dc9776..6dda525855 100644 --- a/redisinsight/ui/src/pages/instance/InstancePage.tsx +++ b/redisinsight/ui/src/pages/instance/InstancePage.tsx @@ -1,7 +1,7 @@ import { EuiResizableContainer } from '@elastic/eui' -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' +import { useLocation, useParams } from 'react-router-dom' import cx from 'classnames' import { setInitialAnalyticsSettings } from 'uiSrc/slices/analytics/settings' @@ -34,6 +34,7 @@ import { setClusterDetailsInitialState } from 'uiSrc/slices/analytics/clusterDet import { setDatabaseAnalysisInitialState } from 'uiSrc/slices/analytics/dbAnalysis' import { resetRedisearchKeysData, setRedisearchInitialState } from 'uiSrc/slices/browser/redisearch' import { setTriggeredFunctionsInitialState } from 'uiSrc/slices/triggeredFunctions/triggeredFunctions' +import { getPageName } from 'uiSrc/utils/routing' import InstancePageRouter from './InstancePageRouter' import styles from './styles.module.scss' @@ -66,12 +67,16 @@ const InstancePage = ({ routes = [] }: Props) => { const [isShouldChildrenRerender, setIsShouldChildrenRerender] = useState(false) const dispatch = useDispatch() + const { pathname } = useLocation() + const { instanceId: connectionInstanceId } = useParams<{ instanceId: string }>() const { isShowCli, isShowHelper } = useSelector(cliSettingsSelector) const { data: modulesData } = useSelector(instancesSelector) const { isShowMonitor } = useSelector(monitorSelector) const { contextInstanceId } = useSelector(appContextSelector) + const lastPageRef = useRef() + const isShowBottomGroup = isShowCli || isShowHelper || isShowMonitor useEffect(() => { @@ -82,9 +87,10 @@ const InstancePage = ({ routes = [] }: Props) => { dispatch(fetchConnectedInstanceInfoAction(connectionInstanceId)) if (contextInstanceId && contextInstanceId !== connectionInstanceId) { - // rerender children from scratch to clear all component states - setIsShouldChildrenRerender(true) - requestAnimationFrame(() => setIsShouldChildrenRerender(false)) + // rerender children only if the same page from scratch to clear all component states + if (lastPageRef.current === getPageName(connectionInstanceId, pathname)) { + setIsShouldChildrenRerender(true) + } resetContext() } @@ -93,6 +99,14 @@ const InstancePage = ({ routes = [] }: Props) => { dispatch(setDbConfig(localStorageService.get(BrowserStorageItem.dbConfig + connectionInstanceId))) }, [connectionInstanceId]) + useEffect(() => { + lastPageRef.current = getPageName(connectionInstanceId, pathname) + }, [pathname]) + + useEffect(() => { + if (isShouldChildrenRerender) setIsShouldChildrenRerender(false) + }, [isShouldChildrenRerender]) + useEffect(() => () => { setSizes((prevSizes: ResizablePanelSize) => { localStorageService.set(BrowserStorageItem.cliResizableContainer, { diff --git a/redisinsight/ui/src/pages/redisCloudDatabases/RedisCloudDatabases/RedisCloudDatabases.tsx b/redisinsight/ui/src/pages/redisCloudDatabases/RedisCloudDatabases/RedisCloudDatabases.tsx index d45c5d5879..82235b5b98 100644 --- a/redisinsight/ui/src/pages/redisCloudDatabases/RedisCloudDatabases/RedisCloudDatabases.tsx +++ b/redisinsight/ui/src/pages/redisCloudDatabases/RedisCloudDatabases/RedisCloudDatabases.tsx @@ -186,7 +186,7 @@ const RedisCloudDatabasesPage = ({

- Redis Enterprise Cloud Databases + Redis Cloud Databases

@@ -195,7 +195,7 @@ const RedisCloudDatabasesPage = ({ These are {' '} {items.length > 1 ? 'databases ' : 'database '} - in your Redis Enterprise cloud. Select the + in your Redis Cloud. Select the {items.length > 1 ? ' databases ' : ' database '} {' '} that you diff --git a/redisinsight/ui/src/pages/redisCloudDatabases/RedisCloudDatabasesPage.tsx b/redisinsight/ui/src/pages/redisCloudDatabases/RedisCloudDatabasesPage.tsx index f2037478ef..7460d02546 100644 --- a/redisinsight/ui/src/pages/redisCloudDatabases/RedisCloudDatabasesPage.tsx +++ b/redisinsight/ui/src/pages/redisCloudDatabases/RedisCloudDatabasesPage.tsx @@ -44,7 +44,7 @@ const RedisCloudDatabasesPage = () => { dataAdded: instancesAdded, } = useSelector(cloudSelector) - setTitle('Redis Enterprise Cloud Databases') + setTitle('Redis Cloud Databases') useEffect(() => { if (instances === null) { diff --git a/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.tsx b/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.tsx index e903928b8e..39fb13c49c 100644 --- a/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.tsx +++ b/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.tsx @@ -47,7 +47,7 @@ interface IPopoverProps { const loadingMsg = 'loading...' const notFoundMsg = 'Not found' -const noResultsMessage = 'Your Redis Enterprise Cloud has no subscriptions available.' +const noResultsMessage = 'Your Redis Cloud has no subscriptions available.' const RedisCloudSubscriptions = ({ subscriptions, @@ -244,7 +244,7 @@ const RedisCloudSubscriptions = ({
-

Redis Enterprise Cloud Subscriptions

+

Redis Cloud Subscriptions

0}> diff --git a/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptionsPage.tsx b/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptionsPage.tsx index ad45bbf034..c9acff8e1f 100644 --- a/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptionsPage.tsx +++ b/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptionsPage.tsx @@ -44,7 +44,7 @@ const RedisCloudSubscriptionsPage = () => { account: { error: accountError, data: account }, } = useSelector(cloudSelector) - setTitle('Redis Enterprise Cloud Subscriptions') + setTitle('Redis Cloud Subscriptions') useEffect(() => { if (subscriptions === null) { diff --git a/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx b/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx index dea2c66625..c68c2a7cec 100644 --- a/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx +++ b/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx @@ -11,14 +11,14 @@ import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { setConnectedInstanceId } from 'uiSrc/slices/instances/instances' import { appInfoSelector } from 'uiSrc/slices/app/info' import { Instance } from 'uiSrc/slices/interfaces' -import AddDatabaseContainer from 'uiSrc/pages/home/components/AddDatabases/AddDatabasesContainer' import { ContentCreateRedis } from 'uiSrc/slices/interfaces/content' import PromoLink from 'uiSrc/components/promo-link/PromoLink' import { getPathToResource } from 'uiSrc/services/resourcesService' -import { HELP_LINKS } from 'uiSrc/pages/home/constants/help-links' +import { HELP_LINKS } from 'uiSrc/pages/home/constants' import { sendEventTelemetry } from 'uiSrc/telemetry' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { contentSelector } from 'uiSrc/slices/content/create-redis-buttons' +import DatabasePanel from 'uiSrc/pages/home/components/database-panel/DatabasePanel' import './styles.scss' import styles from './styles.module.scss' @@ -119,7 +119,7 @@ const EditConnection = () => { )}
- { diff --git a/redisinsight/ui/src/pages/settings/components/cloud-settings/CloudSettings.tsx b/redisinsight/ui/src/pages/settings/components/cloud-settings/CloudSettings.tsx index fa046e3cb2..f128441d22 100644 --- a/redisinsight/ui/src/pages/settings/components/cloud-settings/CloudSettings.tsx +++ b/redisinsight/ui/src/pages/settings/components/cloud-settings/CloudSettings.tsx @@ -54,15 +54,15 @@ const CloudSettings = () => { The list of API user keys that are stored locally in RedisInsight.
- API user keys grant programmatic access to Redis Enterprise Cloud.
- {'To delete API keys from Redis Enterprise Cloud, '} + API user keys grant programmatic access to Redis Cloud.
+ {'To delete API keys from Redis Cloud, '} - sign in to Redis Enterprise Cloud + sign in to Redis Cloud {' and delete them manually.'}
@@ -93,7 +93,7 @@ const CloudSettings = () => {

All API user keys will be removed from RedisInsight.

- {'To delete API keys from Redis Enterprise Cloud, '} + {'To delete API keys from Redis Cloud, '} { tabIndex={-1} href="https://redis.com/redis-enterprise-cloud/overview/?utm_source=redisinsight&utm_medium=settings&utm_campaign=clear_keys" > - sign in to Redis Enterprise Cloud + sign in to Redis Cloud {' and delete them manually.'} diff --git a/redisinsight/ui/src/pages/settings/components/cloud-settings/components/user-api-keys-table/UserApiKeysTable.tsx b/redisinsight/ui/src/pages/settings/components/cloud-settings/components/user-api-keys-table/UserApiKeysTable.tsx index eed7cdf1a4..a78c881443 100644 --- a/redisinsight/ui/src/pages/settings/components/cloud-settings/components/user-api-keys-table/UserApiKeysTable.tsx +++ b/redisinsight/ui/src/pages/settings/components/cloud-settings/components/user-api-keys-table/UserApiKeysTable.tsx @@ -97,7 +97,7 @@ const UserApiKeysTable = ({ items, loading }: Props) => {
{!valid && ( @@ -165,7 +165,7 @@ const UserApiKeysTable = ({ items, loading }: Props) => { header={(<>{formatLongName(name)}
will be removed from RedisInsight.)} text={( <> - {'To delete this API key from Redis Enterprise Cloud, '} + {'To delete this API key from Redis Cloud, '} { tabIndex={-1} href="https://redis.com/redis-enterprise-cloud/overview/?utm_source=redisinsight&utm_medium=settings&utm_campaign=clear_keys" > - sign in to Redis Enterprise Cloud + sign in to Redis Cloud {' and delete it manually.'} @@ -207,7 +207,7 @@ const UserApiKeysTable = ({ items, loading }: Props) => { - Cloud API keys will be created and stored when you connect to Redis Enterprise Cloud to create + Cloud API keys will be created and stored when you connect to Redis Cloud to create a free Cloud database or autodiscover your Cloud database. diff --git a/redisinsight/ui/src/pages/slowLog/components/Actions/Actions.tsx b/redisinsight/ui/src/pages/slowLog/components/Actions/Actions.tsx index 431ed26fe1..b43c928586 100644 --- a/redisinsight/ui/src/pages/slowLog/components/Actions/Actions.tsx +++ b/redisinsight/ui/src/pages/slowLog/components/Actions/Actions.tsx @@ -16,7 +16,7 @@ import { useParams } from 'react-router-dom' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { DurationUnits } from 'uiSrc/constants' import { slowLogSelector } from 'uiSrc/slices/analytics/slowlog' -import AutoRefresh from 'uiSrc/pages/browser/components/auto-refresh' +import { AutoRefresh } from 'uiSrc/components' import { Nullable } from 'uiSrc/utils' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import SlowLogConfig from '../SlowLogConfig' 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 5ed3a800a0..2394ff11f3 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 @@ -5,10 +5,10 @@ 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 { AutoRefresh } from 'uiSrc/components' import styles from './styles.module.scss' export interface Props { 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 122f41e8c3..8b253284fc 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 @@ -10,10 +10,10 @@ import cx from 'classnames' import { useParams } from 'react-router-dom' 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 { AutoRefresh } from 'uiSrc/components' import styles from './styles.module.scss' export interface Props { 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 1e70bf4e2f..6a57a65909 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 @@ -29,7 +29,6 @@ import DeleteLibraryButton from 'uiSrc/pages/triggeredFunctions/pages/Libraries/ 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' @@ -40,6 +39,7 @@ import { } from 'uiSrc/pages/triggeredFunctions/constants' import { Pages } from 'uiSrc/constants' import { getFunctionsLengthByType } from 'uiSrc/utils/triggered-functions/utils' +import { AutoRefresh } from 'uiSrc/components' import styles from './styles.module.scss' export interface Props { diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index d171ce5dd8..689788918a 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -1,13 +1,15 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces' import { ConfigDBStorageItem } from 'uiSrc/constants/storage' -import { getTreeLeafField, Nullable } from 'uiSrc/utils' +import { Nullable } from 'uiSrc/utils' import { BrowserStorageItem, DEFAULT_DELIMITER, DEFAULT_SLOWLOG_DURATION_UNIT, KeyTypes, DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS, + SortOrder, + DEFAULT_TREE_SORTING, } from 'uiSrc/constants' import { localStorageService, setDBConfigStorageField } from 'uiSrc/services' import { RootState } from '../store' @@ -19,6 +21,7 @@ export const initialState: StateAppContext = { lastPage: '', dbConfig: { treeViewDelimiter: DEFAULT_DELIMITER, + treeViewSort: DEFAULT_TREE_SORTING, slowLogDurationUnit: DEFAULT_SLOWLOG_DURATION_UNIT, showHiddenRecommendations: DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS, }, @@ -37,9 +40,8 @@ export const initialState: StateAppContext = { panelSizes: {}, tree: { delimiter: DEFAULT_DELIMITER, - panelSizes: {}, openNodes: {}, - selectedLeaf: {}, + selectedLeaf: null, }, bulkActions: { opened: false, @@ -93,6 +95,7 @@ const appContextSlice = createSlice({ }, setDbConfig: (state, { payload }) => { state.dbConfig.treeViewDelimiter = payload?.treeViewDelimiter ?? DEFAULT_DELIMITER + state.dbConfig.treeViewSort = payload?.treeViewSort ?? DEFAULT_TREE_SORTING state.dbConfig.slowLogDurationUnit = payload?.slowLogDurationUnit ?? DEFAULT_SLOWLOG_DURATION_UNIT state.dbConfig.showHiddenRecommendations = payload?.showHiddenRecommendations ?? DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS @@ -105,6 +108,10 @@ const appContextSlice = createSlice({ state.dbConfig.treeViewDelimiter = payload setDBConfigStorageField(state.contextInstanceId, BrowserStorageItem.treeViewDelimiter, payload) }, + setBrowserTreeSort: (state, { payload }: PayloadAction) => { + state.dbConfig.treeViewSort = payload + setDBConfigStorageField(state.contextInstanceId, BrowserStorageItem.treeViewSort, payload) + }, setRecommendationsShowHidden: (state, { payload }: { payload: boolean }) => { state.dbConfig.showHiddenRecommendations = payload setDBConfigStorageField(state.contextInstanceId, BrowserStorageItem.showHiddenRecommendations, payload) @@ -136,38 +143,9 @@ const appContextSlice = createSlice({ setBrowserPanelSizes: (state, { payload }: { payload: any }) => { state.browser.panelSizes = payload }, - setBrowserTreeSelectedLeaf: (state, { payload }: { payload: any }) => { - state.browser.tree.selectedLeaf = payload - }, - updateBrowserTreeSelectedLeaf: (state, { payload }) => { - const { selectedLeaf, delimiter } = state.browser.tree - const [[selectedLeafField = '', keys = {}]] = Object.entries(selectedLeaf) - const [pattern] = selectedLeafField.split(getTreeLeafField(delimiter)) - - if (payload.key in keys) { - const isFitNewKey = payload.newKey?.startsWith?.(pattern) - && (pattern.split(delimiter)?.length === payload.newKey.split(delimiter)?.length) - - if (!isFitNewKey) { - delete keys[payload.key] - return - } - - keys[payload.newKey] = { - ...keys[payload.key], - name: payload.newKey - } - delete keys[payload.key] - } - - state.browser.tree.selectedLeaf[selectedLeafField] = keys - }, setBrowserTreeNodesOpen: (state, { payload }: { payload: { [key: string]: boolean; } }) => { state.browser.tree.openNodes = payload }, - setBrowserTreePanelSizes: (state, { payload }: { payload: any }) => { - state.browser.tree.panelSizes = payload - }, setWorkbenchScript: (state, { payload }: { payload: string }) => { state.workbench.script = payload }, @@ -196,7 +174,7 @@ const appContextSlice = createSlice({ localStorageService.set(BrowserStorageItem.isEnablementAreaMinimized, payload) }, resetBrowserTree: (state) => { - state.browser.tree.selectedLeaf = {} + state.browser.tree.selectedLeaf = null state.browser.tree.openNodes = {} }, setPubSubFieldsContext: (state, { payload }: { payload: { channel: string, message: string } }) => { @@ -239,12 +217,9 @@ export const { setBrowserRedisearchScrollPosition, setBrowserIsNotRendered, setBrowserPanelSizes, - setBrowserTreeSelectedLeaf, setBrowserTreeNodesOpen, setBrowserTreeDelimiter, - updateBrowserTreeSelectedLeaf, resetBrowserTree, - setBrowserTreePanelSizes, setWorkbenchScript, setWorkbenchVerticalPanelSizes, setLastPageContext, @@ -260,6 +235,7 @@ export const { setDbIndexState, setRecommendationsShowHidden, setLastTriggeredFunctionsPage, + setBrowserTreeSort, } = appContextSlice.actions // Selectors diff --git a/redisinsight/ui/src/slices/browser/bulkActions.ts b/redisinsight/ui/src/slices/browser/bulkActions.ts index 6678bac398..d79db8cdca 100644 --- a/redisinsight/ui/src/slices/browser/bulkActions.ts +++ b/redisinsight/ui/src/slices/browser/bulkActions.ts @@ -86,6 +86,12 @@ const bulkActionsSlice = createSlice({ } }, + setDeleteOverviewStatus: (state, { payload }) => { + if (state.bulkDelete.overview) { + state.bulkDelete.overview.status = payload + } + }, + disconnectBulkDeleteAction: (state) => { state.bulkDelete.loading = false state.bulkDelete.isActionTriggered = false @@ -127,6 +133,7 @@ export const { disconnectBulkDeleteAction, toggleBulkDeleteActionTriggered, setDeleteOverview, + setDeleteOverviewStatus, setBulkActionsInitialState, bulkDeleteSuccess, bulkUpload, diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index e207593f4a..6c09ae4572 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -11,7 +11,8 @@ import { ENDPOINT_BASED_ON_KEY_TYPE, SearchHistoryMode, SortOrder, - STRING_MAX_LENGTH + STRING_MAX_LENGTH, + ModulesKeyTypes } from 'uiSrc/constants' import { getApiErrorMessage, @@ -28,7 +29,7 @@ import { import { DEFAULT_SEARCH_MATCH, SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent, getAdditionalAddedEventData, getMatchType } from 'uiSrc/telemetry' import successMessages from 'uiSrc/components/notifications/success-messages' -import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' +import { IFetchKeyArgs, IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' import { resetBrowserTree } from 'uiSrc/slices/app/context' import { @@ -44,12 +45,12 @@ import { import { CreateStreamDto } from 'apiSrc/modules/browser/dto/stream.dto' import { fetchString } from './string' -import { setZsetInitialState, fetchZSetMembers } from './zset' -import { fetchSetMembers } from './set' +import { setZsetInitialState, fetchZSetMembers, refreshZsetMembersAction } from './zset' +import { fetchSetMembers, refreshSetMembersAction } from './set' import { fetchReJSON } from './rejson' -import { setHashInitialState, fetchHashFields } from './hash' -import { setListInitialState, fetchListElements } from './list' -import { fetchStreamEntries, setStreamInitialState } from './stream' +import { setHashInitialState, fetchHashFields, refreshHashFieldsAction } from './hash' +import { setListInitialState, fetchListElements, refreshListElementsAction } from './list' +import { fetchStreamEntries, refreshStream, setStreamInitialState } from './stream' import { deleteRedisearchHistoryAction, deleteRedisearchKeyFromList, @@ -1027,6 +1028,39 @@ export function fetchKeysMetadata( } } +// Asynchronous thunk action +export function fetchKeysMetadataTree( + keys: RedisString[], + filter: Nullable, + signal?: AbortSignal, + onSuccessAction?: (data: GetKeyInfoResponse[]) => void, + onFailAction?: () => void +) { + return async (_dispatch: AppDispatch, stateInit: () => RootState) => { + try { + const state = stateInit() + const { data } = await apiService.post( + getUrl( + state.connections.instances?.connectedInstance?.id, + ApiEndpoints.KEYS_METADATA + ), + { keys: keys.map(([,nameBuffer]) => nameBuffer), type: filter || undefined }, + { params: { encoding: state.app.info.encoding }, signal } + ) + + const newData = data.map((key, i) => ({ ...key, path: keys[i][0] })) + + onSuccessAction?.(newData) + } catch (_err) { + if (!axios.isCancel(_err)) { + const error = _err as AxiosError + onFailAction?.() + console.error(error) + } + } + } +} + export function fetchPatternHistoryAction( onSuccess?: () => void, onFailed?: () => void, @@ -1216,3 +1250,42 @@ export function editKeyTTLFromList(data: [RedisResponseBuffer, number]) { : dispatch(editRedisearchKeyTTLFromList(data)) } } + +export function refreshKey(key: RedisResponseBuffer, type: KeyTypes | ModulesKeyTypes, args?: IFetchKeyArgs) { + return async (dispatch: AppDispatch) => { + const resetData = false + dispatch(refreshKeyInfoAction(key)) + switch (type) { + case KeyTypes.Hash: { + dispatch(refreshHashFieldsAction(key, resetData)) + break + } + case KeyTypes.ZSet: { + dispatch(refreshZsetMembersAction(key, resetData)) + break + } + case KeyTypes.Set: { + dispatch(refreshSetMembersAction(key, resetData)) + break + } + case KeyTypes.List: { + dispatch(refreshListElementsAction(key, resetData)) + break + } + case KeyTypes.String: { + dispatch(fetchString(key, { resetData, end: args?.end || STRING_MAX_LENGTH })) + break + } + case KeyTypes.ReJSON: { + dispatch(fetchReJSON(key, '.', true)) + break + } + case KeyTypes.Stream: { + dispatch(refreshStream(key, resetData)) + break + } + default: + dispatch(fetchKeyInfo(key, resetData)) + } + } +} diff --git a/redisinsight/ui/src/slices/instances/instances.ts b/redisinsight/ui/src/slices/instances/instances.ts index 81590223fb..775ca7f200 100644 --- a/redisinsight/ui/src/slices/instances/instances.ts +++ b/redisinsight/ui/src/slices/instances/instances.ts @@ -471,7 +471,7 @@ function autoCreateAndConnectToInstanceActionSuccess( } // Asynchronous thunk action -export function updateInstanceAction({ id, ...payload }: Instance, onSuccess?: () => void) { +export function updateInstanceAction({ id, ...payload }: Partial, onSuccess?: () => void) { return async (dispatch: AppDispatch) => { dispatch(defaultInstanceChanging()) diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index e99ce6c1f3..40fb560410 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -1,8 +1,7 @@ import { AxiosError } from 'axios' import { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces' import { Nullable } from 'uiSrc/utils' -import { DurationUnits, FeatureFlags, ICommands } from 'uiSrc/constants' -import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' +import { DurationUnits, FeatureFlags, ICommands, SortOrder } from 'uiSrc/constants' import { GetServerInfoResponse } from 'apiSrc/modules/server/dto/server.dto' import { RedisString as RedisStringAPI } from 'apiSrc/common/constants/redis-string' @@ -50,6 +49,7 @@ export interface StateAppContext { lastPage: string dbConfig: { treeViewDelimiter: string + treeViewSort: SortOrder slowLogDurationUnit: DurationUnits showHiddenRecommendations: boolean } @@ -70,17 +70,10 @@ export interface StateAppContext { } tree: { delimiter: string - panelSizes: { - [key: string]: number - } openNodes: { [key: string]: boolean } - selectedLeaf: { - [key: string]: { - [key: string]: IKeyPropTypes - } - } + selectedLeaf: Nullable } bulkActions: { opened: boolean diff --git a/redisinsight/ui/src/slices/interfaces/cloud.ts b/redisinsight/ui/src/slices/interfaces/cloud.ts index e13553a4f5..c1b4a4fce7 100644 --- a/redisinsight/ui/src/slices/interfaces/cloud.ts +++ b/redisinsight/ui/src/slices/interfaces/cloud.ts @@ -84,5 +84,6 @@ export enum CloudSsoUtmCampaign { BrowserFilter = 'browser_filter', Tutorial = 'tutorial', TriggersAndFunctions = 'redisinsight_triggers_and_functions', + AutoDiscovery = 'auto_discovery', Unknown = 'other', } diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index 5df4bb4edf..a52c6f83c5 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -15,7 +15,7 @@ import { CreateSentinelDatabaseDto } from 'apiSrc/modules/redis-sentinel/dto/cre import { CreateSentinelDatabaseResponse } from 'apiSrc/modules/redis-sentinel/dto/create.sentinel.database.response' import { RedisNodeInfoResponse } from 'apiSrc/modules/database/dto/redis-info.dto' -export interface Instance extends DatabaseInstanceResponse { +export interface Instance extends Partial { host: string port: number nameFromProvider?: Nullable @@ -494,7 +494,7 @@ export interface ICredentialsRedisCloud { export enum InstanceType { Standalone = 'Redis Database', - RedisCloudPro = 'Redis Enterprise Cloud', + RedisCloudPro = 'Redis Cloud', RedisEnterpriseCluster = 'Redis Enterprise Cluster', AWSElasticache = 'AWS Elasticache', Sentinel = 'Redis Sentinel', diff --git a/redisinsight/ui/src/slices/oauth/cloud.ts b/redisinsight/ui/src/slices/oauth/cloud.ts index e6c69cd543..e666c3dbdf 100644 --- a/redisinsight/ui/src/slices/oauth/cloud.ts +++ b/redisinsight/ui/src/slices/oauth/cloud.ts @@ -12,7 +12,6 @@ import { } from 'uiSrc/components/notifications/components' import successMessages from 'uiSrc/components/notifications/success-messages' import { getCloudSsoUtmParams } from 'uiSrc/utils/oauth/cloudSsoUtm' -import { resetKeys } from 'uiSrc/slices/browser/keys' import { CloudUser } from 'apiSrc/modules/cloud/user/models' import { CloudJobInfo } from 'apiSrc/modules/cloud/job/models' import { CloudSubscriptionPlanResponse } from 'apiSrc/modules/cloud/subscription/dto' @@ -33,7 +32,6 @@ import { removeInfiniteNotification } from '../app/notifications' import { checkConnectToInstanceAction, setConnectedInstanceId } from '../instances/instances' -import { setAppContextInitialState } from '../app/context' export const initialState: StateAppOAuth = { loading: false, @@ -245,8 +243,6 @@ export function createFreeDbSuccess(id: string, history: any) { dispatch(removeInfiniteNotification(InfiniteMessagesIds.oAuthSuccess)) if (!isConnected) { - dispatch(resetKeys()) - dispatch(setAppContextInitialState()) dispatch(setConnectedInstanceId(id ?? '')) dispatch(checkConnectToInstanceAction(id)) } diff --git a/redisinsight/ui/src/slices/tests/app/context.spec.ts b/redisinsight/ui/src/slices/tests/app/context.spec.ts index 18a2ac60a8..6f4e6d723e 100644 --- a/redisinsight/ui/src/slices/tests/app/context.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/context.spec.ts @@ -1,6 +1,6 @@ import { cloneDeep } from 'lodash' -import { DEFAULT_DELIMITER, KeyTypes } from 'uiSrc/constants' -import { getTreeLeafField, stringToBuffer } from 'uiSrc/utils' +import { KeyTypes, SortOrder } from 'uiSrc/constants' +import { stringToBuffer } from 'uiSrc/utils' import { cleanup, @@ -16,6 +16,7 @@ import reducer, { setBrowserSelectedKey, setBrowserPatternScrollPosition, setBrowserPanelSizes, + setBrowserTreeSort, setWorkbenchScript, setWorkbenchVerticalPanelSizes, setLastPageContext, @@ -27,11 +28,8 @@ import reducer, { setWorkbenchEAItemScrollTop, resetWorkbenchEASearch, setBrowserTreeNodesOpen, - setBrowserTreePanelSizes, resetBrowserTree, appContextBrowserTree, - setBrowserTreeSelectedLeaf, - updateBrowserTreeSelectedLeaf, setBrowserTreeDelimiter, setBrowserIsNotRendered, setBrowserRedisearchScrollPosition, @@ -477,72 +475,6 @@ describe('slices', () => { }) }) - describe('setBrowserTreeSelectedLeaf', () => { - it('should properly set selected keys in the tree', () => { - // Arrange - const selectedLeaf = { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - test: { - name: 'test', - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - } - } - } - const prevState = { - ...initialState, - browser: { - ...initialState.browser, - tree: { - ...initialState.browser.tree, - selectedLeaf - } - }, - } - - const state = { - ...initialState.browser.tree, - selectedLeaf - } - - // Act - const nextState = reducer(prevState, setBrowserTreeSelectedLeaf(selectedLeaf)) - - // Assert - const rootState = Object.assign(initialStateDefault, { - app: { context: nextState }, - }) - - expect(appContextBrowserTree(rootState)).toEqual(state) - }) - }) - - describe('setBrowserTreePanelSizes', () => { - it('should properly set browser tree panel widths', () => { - // Arrange - const panelSizes = { - first: 50, - second: 400 - } - const state = { - ...initialState.browser.tree, - panelSizes - } - - // Act - const nextState = reducer(initialState, setBrowserTreePanelSizes(panelSizes)) - - // Assert - const rootState = Object.assign(initialStateDefault, { - app: { context: nextState }, - }) - - expect(appContextBrowserTree(rootState)).toEqual(state) - }) - }) - describe('setBrowserIsNotRendered', () => { it('should properly set browser is not rendered value', () => { // Arrange @@ -573,6 +505,7 @@ describe('slices', () => { const data = { slowLogDurationUnit: 'msec', treeViewDelimiter: ':-', + treeViewSort: SortOrder.DESC, showHiddenRecommendations: true, } @@ -637,18 +570,18 @@ describe('slices', () => { }) }) - describe('setRecommendationsShowHidden', () => { - it('should properly set is show hidden live recommendations', () => { + describe('setBrowserTreeSort', () => { + it('should properly set browser tree sorting', () => { // Arrange - const value = true + const sorting = SortOrder.DESC const state = { ...initialState.dbConfig, - showHiddenRecommendations: value + treeViewSort: sorting, } // Act - const nextState = reducer(initialState, setRecommendationsShowHidden(value)) + const nextState = reducer(initialState, setBrowserTreeSort(sorting)) // Assert const rootState = Object.assign(initialStateDefault, { @@ -659,107 +592,30 @@ describe('slices', () => { }) }) - describe('resetBrowserTree', () => { - it('should properly set last page', () => { + describe('setRecommendationsShowHidden', () => { + it('should properly set is show hidden live recommendations', () => { // Arrange - const prevState = { - ...initialState, - browser: { - ...initialState.browser, - tree: { - ...initialState.browser.tree, - openNodes: { - test: true - }, - selectedLeaf: { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - test: { - name: 'test', - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - } - } - } - } - }, - } + const value = true + const state = { - ...initialState.browser.tree, - openNodes: {}, - selectedLeaf: {} + ...initialState.dbConfig, + showHiddenRecommendations: value } // Act - const nextState = reducer(prevState, resetBrowserTree()) + const nextState = reducer(initialState, setRecommendationsShowHidden(value)) // Assert const rootState = Object.assign(initialStateDefault, { app: { context: nextState }, }) - expect(appContextBrowserTree(rootState)).toEqual(state) + expect(appContextDbConfig(rootState)).toEqual(state) }) }) - describe('updateBrowserTreeSelectedLeaf', () => { - it('should properly update selected leaf and add a new fitted key', () => { - const payload = { - key: 'test', - newKey: 'test2' - } - // Arrange - const prevState = { - ...initialState, - browser: { - ...initialState.browser, - tree: { - ...initialState.browser.tree, - selectedLeaf: { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - [payload.key]: { - name: payload.key, - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - } - } - } - } - }, - } - const state = { - ...initialState.browser.tree, - openNodes: {}, - selectedLeaf: { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - [payload.newKey]: { - name: payload.newKey, - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - } } - } - } - - // Act - const nextState = reducer(prevState, updateBrowserTreeSelectedLeaf(payload)) - - // Assert - const rootState = Object.assign(initialStateDefault, { - app: { context: nextState }, - }) - - expect(appContextBrowserTree(rootState)).toEqual(state) - }) - it("should properly update selected leaf and remove old key (new key does't fit)", () => { - const payload = { - key: 'test', - newKey: 'test:2' - } + describe('resetBrowserTree', () => { + it('should properly set last page', () => { // Arrange const prevState = { ...initialState, @@ -767,45 +623,21 @@ describe('slices', () => { ...initialState.browser, tree: { ...initialState.browser.tree, - selectedLeaf: { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - [payload.key]: { - name: payload.key, - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - }, - test2: { - name: 'test2', - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - } - } - } + openNodes: { + test: true + }, + selectedLeaf: 'test', } }, } const state = { ...initialState.browser.tree, openNodes: {}, - selectedLeaf: { - [getTreeLeafField(DEFAULT_DELIMITER)]: { - test2: { - name: 'test2', - type: KeyTypes.Hash, - ttl: 123, - size: 123, - length: 321 - }, - } - } + selectedLeaf: null } // Act - const nextState = reducer(prevState, updateBrowserTreeSelectedLeaf(payload)) + const nextState = reducer(prevState, resetBrowserTree()) // Assert const rootState = Object.assign(initialStateDefault, { diff --git a/redisinsight/ui/src/slices/tests/browser/bulkActions.spec.ts b/redisinsight/ui/src/slices/tests/browser/bulkActions.spec.ts index 645de24d71..80bd4e4155 100644 --- a/redisinsight/ui/src/slices/tests/browser/bulkActions.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/bulkActions.spec.ts @@ -1,6 +1,6 @@ import { cloneDeep } from 'lodash' import { AxiosError } from 'axios' -import { BulkActionsType } from 'uiSrc/constants' +import { BulkActionsStatus, BulkActionsType } from 'uiSrc/constants' import reducer, { bulkActionsSelector, initialState, @@ -19,7 +19,7 @@ import reducer, { bulkUpload, bulkUploadSuccess, bulkUploadFailed, - bulkUploadDataAction, + bulkUploadDataAction, setDeleteOverviewStatus, } from 'uiSrc/slices/browser/bulkActions' import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' import { apiService } from 'uiSrc/services' @@ -271,6 +271,40 @@ describe('bulkActions slice', () => { }) }) + describe('setDeleteOverviewStatus', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + bulkDelete: { + ...initialState.bulkDelete, + overview: { + id: 1, + databaseId: '1', + duration: 300, + status: 'inprogress', + type: BulkActionsType.Delete, + summary: { processed: 1, succeed: 1, failed: 0, errors: [] }, + } + } + } + + const overviewState = { + ...currentState.bulkDelete.overview, + status: BulkActionsStatus.Disconnected + } + + // Act + const nextState = reducer(currentState, setDeleteOverviewStatus(BulkActionsStatus.Disconnected)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { bulkActions: nextState }, + }) + expect(bulkActionsDeleteOverviewSelector(rootState)).toEqual(overviewState) + }) + }) + describe('disconnectBulkDeleteAction', () => { it('should properly set state', () => { // Arrange diff --git a/redisinsight/ui/src/slices/tests/browser/keys.spec.ts b/redisinsight/ui/src/slices/tests/browser/keys.spec.ts index de9b08a1da..c4541897d3 100644 --- a/redisinsight/ui/src/slices/tests/browser/keys.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/keys.spec.ts @@ -1,9 +1,9 @@ import { cloneDeep } from 'lodash' import { AxiosError } from 'axios' -import { KeyTypes, KeyValueFormat } from 'uiSrc/constants' +import { KeyTypes, KeyValueFormat, ModulesKeyTypes } from 'uiSrc/constants' import { apiService } from 'uiSrc/services' import { parseKeysListResponse, stringToBuffer, UTF8ToBuffer } from 'uiSrc/utils' -import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' +import { cleanup, clearStoreActions, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' import { addErrorNotification, addMessageNotification } from 'uiSrc/slices/app/notifications' import successMessages from 'uiSrc/components/notifications/success-messages' import { SearchHistoryItem, SearchMode } from 'uiSrc/slices/interfaces/keys' @@ -18,6 +18,7 @@ import { ListElementDestination, SetStringWithExpireDto, } from 'apiSrc/modules/browser/dto' +import { getString, getStringSuccess } from '../../browser/string' import reducer, { addHashKey, addKey, @@ -52,6 +53,7 @@ import reducer, { fetchKeyInfo, fetchKeys, fetchKeysMetadata, + fetchKeysMetadataTree, fetchMoreKeys, fetchPatternHistoryAction, fetchSearchHistoryAction, @@ -78,8 +80,8 @@ import reducer, { resetKeys, setLastBatchPatternKeys, updateSelectedKeyRefreshTime, + refreshKey, } from '../../browser/keys' -import { getString, getStringSuccess } from '../../browser/string' jest.mock('uiSrc/services', () => ({ ...jest.requireActual('uiSrc/services'), @@ -1065,6 +1067,24 @@ describe('keys slice', () => { }) }) + describe('refreshKey', () => { + it('defaultSelectedKeyAction should be called by default', async () => { + const key = stringToBuffer('key') + + // Act + await store.dispatch( + refreshKey(key, ModulesKeyTypes.Graph) + ) + + // Assert + const expectedActions = [ + refreshKeyInfo(), + defaultSelectedKeyAction() + ] + expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) + }) + }) + describe('thunks', () => { describe('fetchKeys', () => { it('call both loadKeys and loadKeysSuccess when fetch is successed', async () => { @@ -1650,6 +1670,63 @@ describe('keys slice', () => { }) }) + describe('fetchKeysMetadataTree', () => { + it('success to fetch keys metadata', async () => { + // Arrange + const data = [ + { + name: stringToBuffer('key1'), + type: 'hash', + ttl: -1, + size: 100, + path: 0, + length: 100, + }, + { + name: stringToBuffer('key2'), + type: 'hash', + ttl: -1, + size: 150, + path: 1, + length: 100, + }, + { + name: stringToBuffer('key3'), + type: 'hash', + ttl: -1, + size: 110, + path: 2, + length: 100, + }, + ] + const responsePayload = { data, status: 200 } + + const apiServiceMock = jest.fn().mockResolvedValue(responsePayload) + const onSuccessMock = jest.fn() + apiService.post = apiServiceMock + const controller = new AbortController() + + // Act + await store.dispatch( + fetchKeysMetadataTree( + data.map(({ name }, i) => ([i, name])), + null, + controller.signal, + onSuccessMock, + ) + ) + + // Assert + expect(apiServiceMock).toBeCalledWith( + '/databases//keys/get-metadata', + { keys: data.map(({ name }, i) => (name)), type: undefined }, + { params: { encoding: 'buffer' }, signal: controller.signal }, + ) + + expect(onSuccessMock).toBeCalledWith(data) + }) + }) + describe('addKeyIntoList', () => { it('updateKeyList should be called', async () => { // Act @@ -1664,179 +1741,179 @@ describe('keys slice', () => { ] expect(store.getActions()).toEqual(expectedActions) }) + }) - describe('fetchPatternHistoryAction', () => { - it('success fetch history', async () => { - // Arrange - const data: SearchHistoryItem[] = [ - { id: '1', mode: SearchMode.Pattern, filter: { type: 'list', match: '*' } }, - { id: '2', mode: SearchMode.Pattern, filter: { type: 'list', match: '*' } }, - ] - const responsePayload = { data, status: 200 } + describe('fetchPatternHistoryAction', () => { + it('success fetch history', async () => { + // Arrange + const data: SearchHistoryItem[] = [ + { id: '1', mode: SearchMode.Pattern, filter: { type: 'list', match: '*' } }, + { id: '2', mode: SearchMode.Pattern, filter: { type: 'list', match: '*' } }, + ] + const responsePayload = { data, status: 200 } - apiService.get = jest.fn().mockResolvedValue(responsePayload) + apiService.get = jest.fn().mockResolvedValue(responsePayload) - // Act - await store.dispatch(fetchPatternHistoryAction()) + // Act + await store.dispatch(fetchPatternHistoryAction()) - // Assert - const expectedActions = [ - loadSearchHistory(), - loadSearchHistorySuccess(data), - ] - expect(store.getActions()).toEqual(expectedActions) - }) - it('failed to load history', async () => { - // Arrange - const errorMessage = 'some error' - const responsePayload = { - response: { - status: 500, - data: { message: errorMessage }, - }, - } + // Assert + const expectedActions = [ + loadSearchHistory(), + loadSearchHistorySuccess(data), + ] + expect(store.getActions()).toEqual(expectedActions) + }) + it('failed to load history', async () => { + // Arrange + const errorMessage = 'some error' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } - apiService.get = jest.fn().mockRejectedValue(responsePayload) + apiService.get = jest.fn().mockRejectedValue(responsePayload) - // Act - await store.dispatch(fetchPatternHistoryAction()) + // Act + await store.dispatch(fetchPatternHistoryAction()) - // Assert - const expectedActions = [ - loadSearchHistory(), - loadSearchHistoryFailure(), - ] - expect(store.getActions()).toEqual(expectedActions) - }) + // Assert + const expectedActions = [ + loadSearchHistory(), + loadSearchHistoryFailure(), + ] + expect(store.getActions()).toEqual(expectedActions) }) + }) - describe('fetchSearchHistoryAction', () => { - it('success fetch history', async () => { - // Arrange - const data: SearchHistoryItem[] = [ - { id: '1', mode: SearchMode.Pattern, filter: { type: 'list', match: '*' } }, - { id: '2', mode: SearchMode.Pattern, filter: { type: 'list', match: '*' } }, - ] - const responsePayload = { data, status: 200 } + describe('fetchSearchHistoryAction', () => { + it('success fetch history', async () => { + // Arrange + const data: SearchHistoryItem[] = [ + { id: '1', mode: SearchMode.Pattern, filter: { type: 'list', match: '*' } }, + { id: '2', mode: SearchMode.Pattern, filter: { type: 'list', match: '*' } }, + ] + const responsePayload = { data, status: 200 } - apiService.get = jest.fn().mockResolvedValue(responsePayload) + apiService.get = jest.fn().mockResolvedValue(responsePayload) - // Act - await store.dispatch(fetchSearchHistoryAction(SearchMode.Pattern)) + // Act + await store.dispatch(fetchSearchHistoryAction(SearchMode.Pattern)) - // Assert - const expectedActions = [ - loadSearchHistory(), - loadSearchHistorySuccess(data), - ] - expect(store.getActions()).toEqual(expectedActions) - }) - it('failed to load history', async () => { - // Arrange - const errorMessage = 'some error' - const responsePayload = { - response: { - status: 500, - data: { message: errorMessage }, - }, - } + // Assert + const expectedActions = [ + loadSearchHistory(), + loadSearchHistorySuccess(data), + ] + expect(store.getActions()).toEqual(expectedActions) + }) + it('failed to load history', async () => { + // Arrange + const errorMessage = 'some error' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } - apiService.get = jest.fn().mockRejectedValue(responsePayload) + apiService.get = jest.fn().mockRejectedValue(responsePayload) - // Act - await store.dispatch(fetchSearchHistoryAction(SearchMode.Pattern)) + // Act + await store.dispatch(fetchSearchHistoryAction(SearchMode.Pattern)) - // Assert - const expectedActions = [ - loadSearchHistory(), - loadSearchHistoryFailure(), - ] - expect(store.getActions()).toEqual(expectedActions) - }) + // Assert + const expectedActions = [ + loadSearchHistory(), + loadSearchHistoryFailure(), + ] + expect(store.getActions()).toEqual(expectedActions) }) + }) - describe('deletePatternHistoryAction', () => { - it('success delete history', async () => { - // Arrange - const responsePayload = { status: 200 } + describe('deletePatternHistoryAction', () => { + it('success delete history', async () => { + // Arrange + const responsePayload = { status: 200 } - apiService.delete = jest.fn().mockResolvedValue(responsePayload) + apiService.delete = jest.fn().mockResolvedValue(responsePayload) - // Act - await store.dispatch(deletePatternHistoryAction(['1'])) + // Act + await store.dispatch(deletePatternHistoryAction(['1'])) - // Assert - const expectedActions = [ - deleteSearchHistory(), - deleteSearchHistorySuccess(['1']), - ] - expect(store.getActions()).toEqual(expectedActions) - }) - - it('failed to delete history', async () => { - // Arrange - const errorMessage = 'some error' - const responsePayload = { - response: { - status: 500, - data: { message: errorMessage }, - }, - } + // Assert + const expectedActions = [ + deleteSearchHistory(), + deleteSearchHistorySuccess(['1']), + ] + expect(store.getActions()).toEqual(expectedActions) + }) - apiService.delete = jest.fn().mockRejectedValue(responsePayload) + it('failed to delete history', async () => { + // Arrange + const errorMessage = 'some error' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } - // Act - await store.dispatch(deletePatternHistoryAction(['1'])) + apiService.delete = jest.fn().mockRejectedValue(responsePayload) - // Assert - const expectedActions = [ - deleteSearchHistory(), - deleteSearchHistoryFailure(), - ] - expect(store.getActions()).toEqual(expectedActions) - }) + // Act + await store.dispatch(deletePatternHistoryAction(['1'])) + + // Assert + const expectedActions = [ + deleteSearchHistory(), + deleteSearchHistoryFailure(), + ] + expect(store.getActions()).toEqual(expectedActions) }) + }) - describe('deleteSearchHistoryAction', () => { - it('success delete history', async () => { - // Arrange - const responsePayload = { status: 200 } + describe('deleteSearchHistoryAction', () => { + it('success delete history', async () => { + // Arrange + const responsePayload = { status: 200 } - apiService.delete = jest.fn().mockResolvedValue(responsePayload) + apiService.delete = jest.fn().mockResolvedValue(responsePayload) - // Act - await store.dispatch(deleteSearchHistoryAction(SearchMode.Pattern, ['1'])) + // Act + await store.dispatch(deleteSearchHistoryAction(SearchMode.Pattern, ['1'])) - // Assert - const expectedActions = [ - deleteSearchHistory(), - deleteSearchHistorySuccess(['1']), - ] - expect(store.getActions()).toEqual(expectedActions) - }) - - it('failed to delete history', async () => { - // Arrange - const errorMessage = 'some error' - const responsePayload = { - response: { - status: 500, - data: { message: errorMessage }, - }, - } + // Assert + const expectedActions = [ + deleteSearchHistory(), + deleteSearchHistorySuccess(['1']), + ] + expect(store.getActions()).toEqual(expectedActions) + }) - apiService.delete = jest.fn().mockRejectedValue(responsePayload) + it('failed to delete history', async () => { + // Arrange + const errorMessage = 'some error' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } - // Act - await store.dispatch(deleteSearchHistoryAction(SearchMode.Pattern, ['1'])) + apiService.delete = jest.fn().mockRejectedValue(responsePayload) - // Assert - const expectedActions = [ - deleteSearchHistory(), - deleteSearchHistoryFailure(), - ] - expect(store.getActions()).toEqual(expectedActions) - }) + // Act + await store.dispatch(deleteSearchHistoryAction(SearchMode.Pattern, ['1'])) + + // Assert + const expectedActions = [ + deleteSearchHistory(), + deleteSearchHistoryFailure(), + ] + expect(store.getActions()).toEqual(expectedActions) }) }) }) diff --git a/redisinsight/ui/src/styles/components/_forms.scss b/redisinsight/ui/src/styles/components/_forms.scss index df208b6296..1a15b15e31 100644 --- a/redisinsight/ui/src/styles/components/_forms.scss +++ b/redisinsight/ui/src/styles/components/_forms.scss @@ -69,6 +69,11 @@ max-height: 100%; @include euiScrollBar; overflow-y: auto; + + &.contentActive { + border-color: var(--euiColorPrimary) !important; + border-bottom-width: 1px !important; + } } .euiRadio .euiRadio__circle, diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 84bc0e5314..8c954dddf4 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -165,6 +165,7 @@ export enum TelemetryEvent { TREE_VIEW_KEYS_SCANNED_WITH_FILTER_ENABLED = 'TREE_VIEW_KEYS_SCANNED_WITH_FILTER_ENABLED', TREE_VIEW_KEYS_ADDITIONALLY_SCANNED = 'TREE_VIEW_KEYS_ADDITIONALLY_SCANNED', TREE_VIEW_DELIMITER_CHANGED = 'TREE_VIEW_DELIMITER_CHANGED', + TREE_VIEW_KEYS_SORTED = 'TREE_VIEW_KEYS_SORTED', TREE_VIEW_KEY_ADDED = 'TREE_VIEW_KEY_ADDED', TREE_VIEW_KEY_LIST_AUTO_REFRESH_ENABLED = 'TREE_VIEW_KEY_LIST_AUTO_REFRESH_ENABLED', TREE_VIEW_KEY_LIST_AUTO_REFRESH_DISABLED = 'TREE_VIEW_KEY_LIST_AUTO_REFRESH_DISABLED', diff --git a/redisinsight/ui/src/utils/comparisons/diff.ts b/redisinsight/ui/src/utils/comparisons/diff.ts index 5f7fcc4736..b154ed3a47 100644 --- a/redisinsight/ui/src/utils/comparisons/diff.ts +++ b/redisinsight/ui/src/utils/comparisons/diff.ts @@ -23,7 +23,7 @@ export const getFormUpdates = (obj1: UnknownObject = {}, obj2: UnknownObject = { obj1, (result: UnknownObject, value, key) => { if (isObject(value)) { - const diff = getFormUpdates(value, obj2[key]) + const diff = getFormUpdates(value, obj2[key] || {}) if (Object.keys(diff).length) { result[key] = diff diff --git a/redisinsight/ui/src/utils/formatters/valueFormatters.tsx b/redisinsight/ui/src/utils/formatters/valueFormatters.tsx index 3dc88465c6..689a8d239c 100644 --- a/redisinsight/ui/src/utils/formatters/valueFormatters.tsx +++ b/redisinsight/ui/src/utils/formatters/valueFormatters.tsx @@ -20,6 +20,7 @@ import { hexToBuffer, stringToBuffer, binaryToBuffer, + Maybe, } from 'uiSrc/utils' import { reSerializeJSON } from 'uiSrc/utils/formatters/json' @@ -40,7 +41,7 @@ const isFormatEditable = (format: KeyValueFormat) => ![ KeyValueFormat.Pickle, ].includes(format) -const isFullStringLoaded = (currentLength: number, fullLength: number) => currentLength === fullLength +const isFullStringLoaded = (currentLength: Maybe, fullLength: Maybe) => currentLength === fullLength const isNonUnicodeFormatter = (format: KeyValueFormat, isValid: boolean) => { if (format === KeyValueFormat.Msgpack) { diff --git a/redisinsight/ui/src/utils/oauth/cloudSsoUtm.tsx b/redisinsight/ui/src/utils/oauth/cloudSsoUtm.tsx index 5861947fa8..a0b2a5b6bd 100644 --- a/redisinsight/ui/src/utils/oauth/cloudSsoUtm.tsx +++ b/redisinsight/ui/src/utils/oauth/cloudSsoUtm.tsx @@ -24,6 +24,8 @@ export const getCloudSsoUtmCampaign = (source?: string | null): CloudSsoUtmCampa return CloudSsoUtmCampaign.TriggersAndFunctions case OAuthSocialSource.Tutorials: return CloudSsoUtmCampaign.Tutorial + case OAuthSocialSource.Autodiscovery: + return CloudSsoUtmCampaign.AutoDiscovery default: return CloudSsoUtmCampaign.Unknown } diff --git a/redisinsight/ui/src/utils/oauth/parseCloudOAuthError.tsx b/redisinsight/ui/src/utils/oauth/parseCloudOAuthError.tsx index e400b93f49..800ceea1f3 100644 --- a/redisinsight/ui/src/utils/oauth/parseCloudOAuthError.tsx +++ b/redisinsight/ui/src/utils/oauth/parseCloudOAuthError.tsx @@ -81,7 +81,7 @@ export const parseCloudOAuthError = (err: CustomError | string = DEFAULT_ERROR_M title = 'Access denied' message = ( <> - You do not have permission to access Redis Enterprise Cloud. + You do not have permission to access Redis Cloud. ) break @@ -115,7 +115,7 @@ export const parseCloudOAuthError = (err: CustomError | string = DEFAULT_ERROR_M title = 'Unauthorized' message = ( <> - Your Redis Enterprise Cloud authorization failed. + Your Redis Cloud authorization failed. Try again later. @@ -128,11 +128,11 @@ export const parseCloudOAuthError = (err: CustomError | string = DEFAULT_ERROR_M title = 'Invalid API key' message = ( <> - Your Redis Enterprise Cloud authorization failed. + Your Redis Cloud authorization failed. Remove the invalid API key from RedisInsight and try again. - Open the Settings page to manage Redis Enterprise Cloud API keys. + Open the Settings page to manage Redis Cloud API keys. ) additionalInfo.resourceId = err.resourceId @@ -143,7 +143,7 @@ export const parseCloudOAuthError = (err: CustomError | string = DEFAULT_ERROR_M title = 'Database already exists' message = ( <> - You already have a free Redis Enterprise Cloud database running. + You already have a free Redis Cloud database running. Check out your Cloud console for connection details. diff --git a/redisinsight/ui/src/utils/routing.ts b/redisinsight/ui/src/utils/routing.ts index 68cbd3f3a1..0f4dd6670a 100644 --- a/redisinsight/ui/src/utils/routing.ts +++ b/redisinsight/ui/src/utils/routing.ts @@ -43,3 +43,5 @@ export const getRedirectionPage = (pageInput: string, databaseId?: string): Null return `/${page}` } } + +export const getPageName = (databaseId: string, path: string) => path?.replace(`/${databaseId}`, '') diff --git a/redisinsight/ui/src/utils/test-utils.tsx b/redisinsight/ui/src/utils/test-utils.tsx index f1f55c4ed2..ea7cd44ce1 100644 --- a/redisinsight/ui/src/utils/test-utils.tsx +++ b/redisinsight/ui/src/utils/test-utils.tsx @@ -253,6 +253,7 @@ jest.mock('uiSrc/constants/recommendations', () => ({ // mock to not import routes jest.mock('uiSrc/utils/routing', () => ({ + ...jest.requireActual('uiSrc/utils/routing'), getRedirectionPage: jest.fn(), })) @@ -281,7 +282,7 @@ const matchMediaMock = () => ({ Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation(query => matchMediaMock(query)), + value: jest.fn().mockImplementation((query) => matchMediaMock(query)), }) export const getMswResourceURL = (path: string = '') => RESOURCES_BASE_URL.concat(path) diff --git a/redisinsight/ui/src/utils/tests/oauth/cloudSsoUtm.spec.tsx b/redisinsight/ui/src/utils/tests/oauth/cloudSsoUtm.spec.tsx index 4678c0fe38..c231f1ef4c 100644 --- a/redisinsight/ui/src/utils/tests/oauth/cloudSsoUtm.spec.tsx +++ b/redisinsight/ui/src/utils/tests/oauth/cloudSsoUtm.spec.tsx @@ -15,6 +15,7 @@ const getCloudSsoUtmCampaignTestCases = [ [OAuthSocialSource.WelcomeScreen, CloudSsoUtmCampaign.WelcomeScreen], [OAuthSocialSource.TriggersAndFunctions, CloudSsoUtmCampaign.TriggersAndFunctions], [OAuthSocialSource.Tutorials, CloudSsoUtmCampaign.Tutorial], + [OAuthSocialSource.Autodiscovery, CloudSsoUtmCampaign.AutoDiscovery], [null, CloudSsoUtmCampaign.Unknown], [undefined, CloudSsoUtmCampaign.Unknown], ] diff --git a/redisinsight/ui/src/utils/tests/oauth/parseCloudOAuthError.spec.tsx b/redisinsight/ui/src/utils/tests/oauth/parseCloudOAuthError.spec.tsx index f0a53e790d..da432f6ebf 100644 --- a/redisinsight/ui/src/utils/tests/oauth/parseCloudOAuthError.spec.tsx +++ b/redisinsight/ui/src/utils/tests/oauth/parseCloudOAuthError.spec.tsx @@ -27,7 +27,7 @@ const parseCloudOAuthErrorTests = [ title: 'Access denied', message: ( <> - You do not have permission to access Redis Enterprise Cloud. + You do not have permission to access Redis Cloud. ) })], @@ -60,7 +60,7 @@ const parseCloudOAuthErrorTests = [ title: 'Unauthorized', message: ( <> - Your Redis Enterprise Cloud authorization failed. + Your Redis Cloud authorization failed. Try again later. @@ -78,7 +78,7 @@ const parseCloudOAuthErrorTests = [ title: 'Database already exists', message: ( <> - You already have a free Redis Enterprise Cloud database running. + You already have a free Redis Cloud database running. Check out your Cloud console for connection details. @@ -89,11 +89,11 @@ const parseCloudOAuthErrorTests = [ title: 'Invalid API key', message: ( <> - Your Redis Enterprise Cloud authorization failed. + Your Redis Cloud authorization failed. Remove the invalid API key from RedisInsight and try again. - Open the Settings page to manage Redis Enterprise Cloud API keys. + Open the Settings page to manage Redis Cloud API keys. ), additionalInfo: { diff --git a/redisinsight/ui/src/utils/tests/routing.spec.ts b/redisinsight/ui/src/utils/tests/routing.spec.ts index 6f823102b9..d0f203bf4a 100644 --- a/redisinsight/ui/src/utils/tests/routing.spec.ts +++ b/redisinsight/ui/src/utils/tests/routing.spec.ts @@ -1,4 +1,4 @@ -import { getRedirectionPage } from 'uiSrc/utils/routing' +import { getPageName, getRedirectionPage } from 'uiSrc/utils/routing' jest.mock('uiSrc/utils/routing', () => ({ ...jest.requireActual('uiSrc/utils/routing') @@ -25,3 +25,20 @@ describe('getRedirectionPage', () => { } ) }) + +const getPageNameTests = [ + { input: ['instanceId', '/instanceId/page1'], expected: '/page1' }, + { input: ['instanceId', '/instanceId/page1/page2'], expected: '/page1/page2' }, + { input: ['instanceId', '/page1'], expected: '/page1' }, +] + +describe('getPageName', () => { + test.each(getPageNameTests)( + '%j', + ({ input, expected }) => { + // @ts-ignore + const result = getPageName(...input) + expect(result).toEqual(expected) + } + ) +}) diff --git a/redisinsight/ui/src/utils/tests/tree.spec.ts b/redisinsight/ui/src/utils/tests/tree.spec.ts index 9eb64953d9..4c04963a49 100644 --- a/redisinsight/ui/src/utils/tests/tree.spec.ts +++ b/redisinsight/ui/src/utils/tests/tree.spec.ts @@ -1,5 +1,4 @@ -import { findTreeNode, getTreeLeafField } from 'uiSrc/utils' -import nodes from './nodes.json' +import { getTreeLeafField } from 'uiSrc/utils' const getTreeLeafFieldTests: any[] = [ [':', 'keys:keys'], @@ -19,23 +18,3 @@ describe('getTreeLeafField', () => { expect(result).toBe(expected) }) }) - -const findTreeNodeTests: any[] = [ - ['hash2:keys:keys:', 'id', null], - ['hash2:keys:keys:', 'fullName', nodes[1]?.children[0]], - ['hash:string:', 'fullName', nodes[0]?.children[1]], - ['hash:string:keys:keys:', 'fullName', nodes[0]?.children[1]?.children[0]], - ['0.g9y9ox4nau', 'id', nodes[0]?.children[0]], - ['hash2:keys:keys:', 'id', null], - ['uoeuoeuoe', 'id', null], - ['uoeuoeuoe', 'fullName', null], - ['hash2:', 'fullName', nodes[1]], -] - -describe('findTreeNode', () => { - it.each(findTreeNodeTests)('for input: %s (reply), should be output: %s', - (reply, key, expected) => { - const result = findTreeNode(nodes, reply, key) - expect(result).toBe(expected) - }) -}) diff --git a/redisinsight/ui/src/utils/tree.ts b/redisinsight/ui/src/utils/tree.ts index 817cd5d600..fc235737a7 100644 --- a/redisinsight/ui/src/utils/tree.ts +++ b/redisinsight/ui/src/utils/tree.ts @@ -1,26 +1 @@ -import { TreeNode } from 'uiSrc/components/virtual-tree' -import { Nullable } from './types' - export const getTreeLeafField = (delimiter = '') => `keys${delimiter}keys` - -export const findTreeNode = ( - data: TreeNode[], - value: string, - key = 'id', - tempObj: { found?: TreeNode } = {}, -): Nullable => { - if (value && data) { - // eslint-disable-next-line sonarjs/no-ignored-return - data.find((node) => { - if (node[key] === value) { - tempObj.found = node - return node - } - return findTreeNode(node.children, value, key, tempObj) - }) - if (tempObj.found) { - return tempObj.found - } - } - return null -} diff --git a/redisinsight/ui/src/utils/validations.ts b/redisinsight/ui/src/utils/validations.ts index bc162234cb..70ae4226c5 100644 --- a/redisinsight/ui/src/utils/validations.ts +++ b/redisinsight/ui/src/utils/validations.ts @@ -111,7 +111,7 @@ export const errorValidateNegativeInteger = (value: string) => { } export const validateCertName = (initValue: string) => - initValue.replace(/[^ a-zA-Z0-9!@#$%^&*-_()[\]]+/gi, '').toString() + initValue.replace(/[^ a-zA-Z0-9!@#$%^&*\-_()[\]]+/gi, '').toString() export const isRequiredStringsValid = (...params: string[]) => params.every((p = '') => p.length > 0) diff --git a/resources/icon-tray-colored.png b/resources/icon-tray-colored.png index a6c3154729..4511d9cadb 100644 Binary files a/resources/icon-tray-colored.png and b/resources/icon-tray-colored.png differ diff --git a/resources/icon-tray-white.png b/resources/icon-tray-white.png index c7d906a4bd..01bc7d5ddf 100644 Binary files a/resources/icon-tray-white.png and b/resources/icon-tray-white.png differ diff --git a/resources/icon.png b/resources/icon.png index 00fc47f1d3..e8d189cce3 100644 Binary files a/resources/icon.png and b/resources/icon.png differ diff --git a/resources/icons/128x128.png b/resources/icons/128x128.png index accebeedb1..f5e66fef54 100644 Binary files a/resources/icons/128x128.png and b/resources/icons/128x128.png differ diff --git a/resources/icons/16x16.png b/resources/icons/16x16.png index 70017b408a..78b4236698 100644 Binary files a/resources/icons/16x16.png and b/resources/icons/16x16.png differ diff --git a/resources/icons/24x24.png b/resources/icons/24x24.png index 7e6655fb5d..26fd2cbb1c 100644 Binary files a/resources/icons/24x24.png and b/resources/icons/24x24.png differ diff --git a/resources/icons/256x256.png b/resources/icons/256x256.png index 175ec9b4fc..fc5e0e9f68 100644 Binary files a/resources/icons/256x256.png and b/resources/icons/256x256.png differ diff --git a/resources/icons/32x32.png b/resources/icons/32x32.png index 3d6c68d7a6..f976294a0e 100644 Binary files a/resources/icons/32x32.png and b/resources/icons/32x32.png differ diff --git a/resources/icons/48x48.png b/resources/icons/48x48.png index 13a3d06f97..d909fb9171 100644 Binary files a/resources/icons/48x48.png and b/resources/icons/48x48.png differ diff --git a/resources/icons/512x512.png b/resources/icons/512x512.png index 00fc47f1d3..e8d189cce3 100644 Binary files a/resources/icons/512x512.png and b/resources/icons/512x512.png differ diff --git a/resources/icons/64x64.png b/resources/icons/64x64.png index de0765d924..ee6b980860 100644 Binary files a/resources/icons/64x64.png and b/resources/icons/64x64.png differ diff --git a/resources/icons/96x96.png b/resources/icons/96x96.png index 7f4360fc2f..51014f45bd 100644 Binary files a/resources/icons/96x96.png and b/resources/icons/96x96.png differ diff --git a/tests/e2e/.desktop.env b/tests/e2e/.desktop.env index a4f603c76c..c9c1d6451c 100644 --- a/tests/e2e/.desktop.env +++ b/tests/e2e/.desktop.env @@ -1,7 +1,7 @@ COMMON_URL=https://localhost:5530 API_URL=https://localhost:5530/api OSS_SENTINEL_PASSWORD=password -APP_FOLDER_NAME=.redisinsight-v2-stage +APP_FOLDER_NAME=.redisinsight-app-stage OSS_STANDALONE_HOST=localhost OSS_STANDALONE_PORT=8100 diff --git a/tests/e2e/.env b/tests/e2e/.env index 9e354b1b99..9774b872a8 100644 --- a/tests/e2e/.env +++ b/tests/e2e/.env @@ -1,7 +1,7 @@ COMMON_URL=https://app:5000 API_URL=https://app:5000/api OSS_SENTINEL_PASSWORD=password -APP_FOLDER_NAME=.redisinsight-v2 +APP_FOLDER_NAME=.redisinsight-app NOTIFICATION_UPDATE_URL=https://s3.amazonaws.com/redisinsight.test/public/tests/e2e/notifications.json NOTIFICATION_SYNC_INTERVAL=30000 RI_FEATURES_CONFIG_URL=http://static-server:5551/remote/features-config.json diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore index c61413375a..c3f7d370d2 100644 --- a/tests/e2e/.gitignore +++ b/tests/e2e/.gitignore @@ -2,4 +2,6 @@ plugins report results remote -.redisinsight-v2 \ No newline at end of file +.redisinsight +.redisinsight-v2 +.redisinsight-app diff --git a/tests/e2e/common-actions/browser-actions.ts b/tests/e2e/common-actions/browser-actions.ts index 4eb63aacc5..fcc6aceaaf 100644 --- a/tests/e2e/common-actions/browser-actions.ts +++ b/tests/e2e/common-actions/browser-actions.ts @@ -1,4 +1,4 @@ -import {Selector, t} from 'testcafe'; +import { Selector, t } from 'testcafe'; import { BrowserPage } from '../pageObjects'; const browserPage = new BrowserPage(); @@ -29,6 +29,7 @@ export class BrowserActions { } } } + /** * Verify tooltip contains text * @param expectedText Expected link that is compared with actual @@ -39,17 +40,88 @@ export class BrowserActions { ? await t.expect(browserPage.tooltip.textContent).contains(expectedText, `"${expectedText}" Text is incorrect in tooltip`) : await t.expect(browserPage.tooltip.textContent).notContains(expectedText, `Tooltip still contains text "${expectedText}"`); } + /** * Verify that the new key is displayed at the top of the list of keys and opened and pre-selected in List view - * */ + * @param keyName Key name + */ async verifyKeyDisplayedTopAndOpened(keyName: string): Promise { await t.expect(Selector('[aria-rowindex="1"]').withText(keyName).visible).ok(`element with ${keyName} is not visible in the top of list`); await t.expect(browserPage.keyNameFormDetails.withText(keyName).visible).ok(`element with ${keyName} is not opened`); } + /** * Verify that the new key is not displayed at the top of the list of keys and opened and pre-selected in List view - * */ + * @param keyName Key name + */ async verifyKeyIsNotDisplayedTop(keyName: string): Promise { await t.expect(Selector('[aria-rowindex="1"]').withText(keyName).exists).notOk(`element with ${keyName} is not visible in the top of list`); } + + /** + * Verify that not patterned keys not visible with delimiter + * @param delimiter string with delimiter value + */ + async verifyNotPatternedKeys(delimiter: string): Promise { + const notPatternedKeys = Selector('[data-testid^="badge"]').parent('[data-testid^="node-item_"]'); + const notPatternedKeysNumber = await notPatternedKeys.count; + + for (let i = 0; i < notPatternedKeysNumber; i++) { + await t.expect(notPatternedKeys.nth(i).withText(delimiter).exists).notOk('Not contained delimiter keys'); + } + } + + /** + * Get node name by folders + * @param startFolder start folder + * @param folderName name of folder + * @param delimiter string with delimiter value + */ + getNodeName(startFolder: string, folderName: string, delimiter: string): string { + return startFolder + folderName + delimiter; + + } + + /** + * Get node selector by name + * @param name node name + */ + getNodeSelector(name: string): Selector { + return Selector(`[data-testid^="node-item_${name}"]`); + } + + /** + * Check tree view structure + * @param folders name of folders for tree view build + * @param delimiter string with delimiter value + */ + async checkTreeViewFoldersStructure(folders: string[][], delimiter: string): Promise { + // Verify not patterned keys + await this.verifyNotPatternedKeys(delimiter); + + const foldersNumber = folders.length; + + for (let i = 0; i < foldersNumber; i++) { + const innerFoldersNumber = folders[i].length; + let prevNodeSelector = ''; + + for (let j = 0; j < innerFoldersNumber; j++) { + const nodeName = this.getNodeName(prevNodeSelector, folders[i][j], delimiter); + const node = this.getNodeSelector(nodeName); + const fullTestIdSelector = await node.getAttribute('data-testid'); + if (!fullTestIdSelector?.includes('expanded')) { + await t.click(node); + } + prevNodeSelector = nodeName; + } + + // Verify that the last folder level contains required keys + const foundKeyName = `${folders[i].join(delimiter)}`; + const firstFolderName = this.getNodeName('', folders[i][0], delimiter); + const firstFolder = this.getNodeSelector(firstFolderName); + await t + .expect(Selector(`[data-testid*="node-item_${foundKeyName}"]`).find('[data-testid^="key-"]').exists).ok('Specific key not found') + .click(firstFolder); + } + } } diff --git a/tests/e2e/common-actions/workbench-actions.ts b/tests/e2e/common-actions/workbench-actions.ts index 4d495c1102..14242166b8 100644 --- a/tests/e2e/common-actions/workbench-actions.ts +++ b/tests/e2e/common-actions/workbench-actions.ts @@ -1,4 +1,4 @@ -import {t, Selector} from 'testcafe'; +import { t, Selector } from 'testcafe'; import { WorkbenchPage } from '../pageObjects'; const workbenchPage = new WorkbenchPage(); diff --git a/tests/e2e/desktop.runner.ts b/tests/e2e/desktop.runner.ts index 11444f3694..15effadce6 100644 --- a/tests/e2e/desktop.runner.ts +++ b/tests/e2e/desktop.runner.ts @@ -6,16 +6,16 @@ import testcafe from 'testcafe'; return t .createRunner() .compilerOptions({ - "typescript": { + 'typescript': { configPath: 'tsconfig.testcafe.json', experimentalDecorators: true - }}) + } }) .src((process.env.TEST_FILES || 'tests/electron/**/*.e2e.ts').split('\n')) .browsers(['electron']) .screenshots({ path: './report/screenshots/', takeOnFails: true, - pathPattern: '${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png', + pathPattern: '${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png' }) .reporter([ 'spec', @@ -38,13 +38,15 @@ import testcafe from 'testcafe'; selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, + quarantineMode: { successThreshold: 1, attemptLimit: 3 }, + disableMultipleWindows: true }); }) .then((failedCount) => { process.exit(failedCount); }) .catch((e) => { - console.error(e) + console.error(e); process.exit(1); }); })(); diff --git a/tests/e2e/desktop.runner.win.ts b/tests/e2e/desktop.runner.win.ts index 20ec80beb1..edb738b2a0 100644 --- a/tests/e2e/desktop.runner.win.ts +++ b/tests/e2e/desktop.runner.win.ts @@ -6,16 +6,16 @@ import testcafe from 'testcafe'; return t .createRunner() .compilerOptions({ - "typescript": { + 'typescript': { configPath: 'tsconfig.testcafe.json', experimentalDecorators: true - }}) + } }) .src((process.env.TEST_FILES || 'tests/electron/**/*.e2e.ts').split('\n')) .browsers(['electron']) .screenshots({ path: 'report/screenshots/', takeOnFails: true, - pathPattern: '${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png', + pathPattern: '${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png' }) .reporter([ 'spec', @@ -38,6 +38,8 @@ import testcafe from 'testcafe'; selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, + quarantineMode: { successThreshold: 1, attemptLimit: 3 }, + disableMultipleWindows: true }); }) .then((failedCount) => { diff --git a/tests/e2e/docker.web.docker-compose.yml b/tests/e2e/docker.web.docker-compose.yml index b139d5c337..9cd09a2e2f 100644 --- a/tests/e2e/docker.web.docker-compose.yml +++ b/tests/e2e/docker.web.docker-compose.yml @@ -10,7 +10,7 @@ services: - ./results:/usr/src/app/results - ./report:/usr/src/app/report - ./plugins:/usr/src/app/plugins - - .redisinsight-v2:/root/.redisinsight-v2 + - .redisinsight-app:/root/.redisinsight-app - .ritmp:/tmp - ./test-data/certs:/root/certs - ./test-data/ssh:/root/ssh @@ -33,7 +33,7 @@ services: command: [ './wait-for-it.sh', 'redis-enterprise:12000', '-s', '-t', '120', '--', - 'npx', 'yarn', 'test:chrome:ci' + 'npm', 'run', 'test:chrome:ci' ] # Built image @@ -44,7 +44,7 @@ services: env_file: - ./.env volumes: - - .redisinsight-v2:/root/.redisinsight-v2 + - .redisinsight-app:/root/.redisinsight-app - .ritmp:/tmp - ./test-data/certs:/root/certs - ./test-data/ssh:/root/ssh diff --git a/tests/e2e/helpers/api/api-database.ts b/tests/e2e/helpers/api/api-database.ts index e91051b7ea..c77262f827 100644 --- a/tests/e2e/helpers/api/api-database.ts +++ b/tests/e2e/helpers/api/api-database.ts @@ -27,28 +27,28 @@ export class DatabaseAPIRequests { ): Promise { const uniqueId = chance.string({ length: 10 }); const requestBody: { - name?: string; - host: string; - port: number; - username?: string; - password?: string; - tls?: boolean; - verifyServerCert?: boolean; + name?: string, + host: string, + port: number, + username?: string, + password?: string, + tls?: boolean, + verifyServerCert?: boolean, caCert?: { - name: string; - certificate?: string; - }; + name: string, + certificate?: string + }, clientCert?: { - name: string; - certificate?: string; - key?: string; - }; + name: string, + certificate?: string, + key?: string + } } = { name: databaseParameters.databaseName, host: databaseParameters.host, port: Number(databaseParameters.port), username: databaseParameters.databaseUsername, - password: databaseParameters.databasePassword, + password: databaseParameters.databasePassword }; if (databaseParameters.caCert) { diff --git a/tests/e2e/helpers/api/api-info.ts b/tests/e2e/helpers/api/api-info.ts index 7d41fdbd7f..978b0e9629 100644 --- a/tests/e2e/helpers/api/api-info.ts +++ b/tests/e2e/helpers/api/api-info.ts @@ -1,6 +1,6 @@ import { t } from 'testcafe'; -import { sendPostRequest } from './api-common'; import { ResourcePath } from '../constants'; +import { sendPostRequest } from './api-common'; /** * Synchronize features diff --git a/tests/e2e/helpers/common.ts b/tests/e2e/helpers/common.ts index 4505f631dc..9fcbd4a171 100644 --- a/tests/e2e/helpers/common.ts +++ b/tests/e2e/helpers/common.ts @@ -1,6 +1,6 @@ import * as path from 'path'; -import * as archiver from 'archiver'; import * as fs from 'fs'; +import * as archiver from 'archiver'; import { ClientFunction, RequestMock, t } from 'testcafe'; import { Chance } from 'chance'; import { apiUrl, commonUrl } from './conf'; @@ -13,7 +13,6 @@ declare global { } } - const settingsApiUrl = `${commonUrl}/api/settings`; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation] const mockedSettingsResponse = { diff --git a/tests/e2e/helpers/conf.ts b/tests/e2e/helpers/conf.ts index f4ee80f89a..fd3a73ae19 100644 --- a/tests/e2e/helpers/conf.ts +++ b/tests/e2e/helpers/conf.ts @@ -9,7 +9,7 @@ export const commonUrl = process.env.COMMON_URL || 'https://localhost:5000'; export const apiUrl = process.env.API_URL || 'https://localhost:5000/api'; export const workingDirectory = process.env.APP_FOLDER_ABSOLUTE_PATH - || (joinPath(os.homedir(), process.env.APP_FOLDER_NAME || '.redisinsight-v2')); + || (joinPath(os.homedir(), process.env.APP_FOLDER_NAME || '.redisinsight-app')); export const fileDownloadPath = joinPath(os.homedir(), 'Downloads'); const uniqueId = chance.string({ length: 10 }); diff --git a/tests/e2e/helpers/database-scripts.ts b/tests/e2e/helpers/database-scripts.ts index 9bc7ff95a9..faba4a431d 100644 --- a/tests/e2e/helpers/database-scripts.ts +++ b/tests/e2e/helpers/database-scripts.ts @@ -1,5 +1,5 @@ -import { workingDirectory } from '../helpers/conf'; import * as sqlite3 from 'sqlite3'; +import { workingDirectory } from '../helpers/conf'; const dbPath = `${workingDirectory}/redisinsight.db`; @@ -17,7 +17,8 @@ export async function updateColumnValueInDBTable(tableName: string, columnName: db.run(query, (err: { message: string }) => { if (err) { reject(new Error(`Error during changing ${columnName} column value: ${err.message}`)); - } else { + } + else { db.close(); resolve(); } @@ -38,7 +39,8 @@ export async function getColumnValueFromTableInDB(tableName: string, columnName: db.get(query, (err: { message: string }, row: any) => { if (err) { reject(new Error(`Error during getting ${columnName} column value: ${err.message}`)); - } else { + } + else { const columnValue = row[columnName]; db.close(); resolve(columnValue); @@ -57,11 +59,11 @@ export async function deleteRowsFromTableInDB(tableName: string): Promise return new Promise((resolve, reject) => { - db.run(query, (err: { message: string }) => { if (err) { reject(new Error(`Error during ${tableName} table rows deletion: ${err.message}`)); - } else { + } + else { db.close(); resolve(); } diff --git a/tests/e2e/helpers/database.ts b/tests/e2e/helpers/database.ts index b92538acc0..d4afdb1819 100644 --- a/tests/e2e/helpers/database.ts +++ b/tests/e2e/helpers/database.ts @@ -132,7 +132,7 @@ export class DatabaseHelper { } /** - * Add a new database from Redis Enterprise Cloud via auto-discover flow + * Add a new database from Redis Cloud via auto-discover flow * @param cloudAPIAccessKey The Cloud API Access Key * @param cloudAPISecretKey The Cloud API Secret Key */ @@ -151,7 +151,7 @@ export class DatabaseHelper { await t .expect( autoDiscoverREDatabases.title.withExactText( - 'Redis Enterprise Cloud Subscriptions' + 'Redis Cloud Subscriptions' ).exists ) .ok('Subscriptions list not displayed', { timeout: 120000 }); diff --git a/tests/e2e/helpers/insights.ts b/tests/e2e/helpers/insights.ts index 6a3709d83b..240dedf2bf 100644 --- a/tests/e2e/helpers/insights.ts +++ b/tests/e2e/helpers/insights.ts @@ -1,5 +1,5 @@ -import * as fs from 'fs-extra'; import * as path from 'path'; +import * as fs from 'fs-extra'; import { BasePage } from '../pageObjects'; import { deleteRowsFromTableInDB, updateColumnValueInDBTable } from './database-scripts'; import { syncFeaturesApi } from './api/api-info'; diff --git a/tests/e2e/helpers/keys.ts b/tests/e2e/helpers/keys.ts index 517d45a6f2..8e1fa94393 100644 --- a/tests/e2e/helpers/keys.ts +++ b/tests/e2e/helpers/keys.ts @@ -249,21 +249,13 @@ export async function deleteAllKeysFromDB(host: string, port: string): Promise { +export async function verifyKeysDisplayingInTheList(keyNames: string[], isDisplayed: boolean): Promise { for (const keyName of keyNames) { - await t.expect(browserPage.getKeySelectorByName(keyName).exists).ok(`The key ${keyName} not found`); - } -} - -/** -* Verifying if the Keys are not in the List of keys -* @param keyNames The names of the keys -*/ - -export async function verifyKeysNotDisplayedInTheList(keyNames: string[]): Promise { - for (const keyName of keyNames) { - await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk(`The key ${keyName} found`); + isDisplayed + ? await t.expect(browserPage.getKeySelectorByName(keyName).exists).ok(`The key ${keyName} not found`) + : await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk(`The key ${keyName} found`); } } @@ -271,7 +263,6 @@ export async function verifyKeysNotDisplayedInTheList(keyNames: string[]): Promi * Verify search/filter value * @param value The value in search/filter input */ - export async function verifySearchFilterValue(value: string): Promise { await t.expect(browserPage.filterByPatterSearchInput.withAttribute('value', value).exists).ok(`Filter per key name ${value} is not applied/correct`); } diff --git a/tests/e2e/helpers/notifications.ts b/tests/e2e/helpers/notifications.ts index ad17268074..b619b350ce 100644 --- a/tests/e2e/helpers/notifications.ts +++ b/tests/e2e/helpers/notifications.ts @@ -1,4 +1,4 @@ -import { workingDirectory} from '../helpers/conf'; +import { workingDirectory } from '../helpers/conf'; import { NotificationParameters } from '../pageObjects/components/navigation/notification-panel'; const dbPath = `${workingDirectory}/redisinsight.db`; diff --git a/tests/e2e/helpers/utils.ts b/tests/e2e/helpers/utils.ts new file mode 100644 index 0000000000..addb1a66ad --- /dev/null +++ b/tests/e2e/helpers/utils.ts @@ -0,0 +1,3 @@ +import { ClientFunction } from 'testcafe'; + +export const goBackHistory = ClientFunction(() => window.history.back()); diff --git a/tests/e2e/local.web.docker-compose.yml b/tests/e2e/local.web.docker-compose.yml index d92bf1b7c5..361f8a0acd 100644 --- a/tests/e2e/local.web.docker-compose.yml +++ b/tests/e2e/local.web.docker-compose.yml @@ -9,7 +9,7 @@ services: volumes: - ./results:/usr/src/app/results - ./plugins:/usr/src/app/plugins - - .redisinsight-v2:/root/.redisinsight-v2 + - .redisinsight-app:/root/.redisinsight-app - ./test-data/certs:/root/certs - ./test-data/ssh:/root/ssh - ./remote:/root/remote @@ -33,7 +33,7 @@ services: command: [ './wait-for-it.sh', 'redis-enterprise:12000', '-s', '-t', '120', '--', - 'npx', 'yarn', 'test:chrome:ci' + 'npm', 'run', 'test:chrome:ci' ] # Redisinsight API + UI build @@ -46,7 +46,7 @@ services: context: ./../../ dockerfile: Dockerfile volumes: - - .redisinsight-v2:/root/.redisinsight-v2 + - .redisinsight-app:/root/.redisinsight-app - ./test-data/certs:/root/certs - ./test-data/ssh:/root/ssh ports: diff --git a/tests/e2e/package.json b/tests/e2e/package.json index d05c168430..d2ac12d0f5 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -13,7 +13,7 @@ "build:web": "yarn --cwd ../../ build:web", "redis:last": "docker run --name redis-last-version -p 7777:6379 -d redislabs/redismod", "start:app": "cross-env SERVER_STATIC_CONTENT=true yarn start:api", - "test:chrome": "testcafe --compiler-options typescript.configPath=tsconfig.testcafe.json --cache --concurrency 1 chrome tests/ -r html:./report/report.html,spec -e -s takeOnFails=true,path=report/screenshots/,pathPattern=${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png", + "test:chrome": "testcafe --compiler-options typescript.configPath=tsconfig.testcafe.json --cache --disable-multiple-windows --concurrency 1 chrome tests/ -r html:./report/report.html,spec -e -s takeOnFails=true,path=report/screenshots/,pathPattern=${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png", "test:chrome:ci": "ts-node ./web.runner.ts", "test": "yarn test:chrome", "lint": "eslint . --ext .ts,.js,.tsx,.jsx", @@ -30,9 +30,7 @@ "@types/lodash": "4.14.192", "@types/node": "20.3.1", "word-wrap": "1.2.4", - "**/semver": "^7.5.2", - "testcafe-hammerhead": "31.4.5", - "testcafe-hammerhead/tough-cookie": "^4.1.3" + "**/semver": "^7.5.2" }, "devDependencies": { "@types/archiver": "^5.3.2", @@ -56,11 +54,11 @@ "redis": "3.1.1", "sqlite3": "^5.1.6", "supertest": "^4.0.2", - "testcafe": "3.0.0", + "testcafe": "3.3.0", "testcafe-browser-provider-electron": "0.0.19", "testcafe-reporter-html": "1.4.6", "testcafe-reporter-json": "2.2.0", - "testcafe-reporter-spec": "2.1.1", + "testcafe-reporter-spec": "2.2.0", "ts-node": "10.9.1", "typescript": "5.1.3" } diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 141888d2c0..8d698561df 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -1,10 +1,11 @@ import { t, Selector } from 'testcafe'; import { Common } from '../helpers/common'; import { InstancePage } from './instance-page'; -import { BulkActions } from './components/browser'; +import { BulkActions, TreeView } from './components/browser'; export class BrowserPage extends InstancePage { BulkActions = new BulkActions(); + TreeView = new TreeView(); //CSS Selectors cssSelectorGrid = '[aria-label="grid"]'; @@ -38,7 +39,7 @@ export class BrowserPage extends InstancePage { refreshKeysButton = Selector('[data-testid=refresh-keys-btn]'); refreshKeyButton = Selector('[data-testid=refresh-key-btn]'); editKeyNameButton = Selector('[data-testid=edit-key-btn]'); - editKeyValueButton = Selector('[data-testid=edit-key-value-btn]'); + editKeyValueButton = Selector('[data-testid=edit-key-value-btn]', { timeout: 500 }); closeKeyButton = Selector('[data-testid=close-key-btn]'); plusAddKeyButton = Selector('[data-testid=btn-add-key]'); addKeyValueItemsButton = Selector('[data-testid=add-key-value-items-btn]'); @@ -71,13 +72,9 @@ export class BrowserPage extends InstancePage { databaseInfoIcon = Selector('[data-testid=db-info-icon]'); treeViewButton = Selector('[data-testid=view-type-list-btn]'); browserViewButton = Selector('[data-testid=view-type-browser-btn]'); - treeViewSeparator = Selector('[data-testid=tree-view-delimiter-btn]'); searchButton = Selector('[data-testid=search-btn]'); clearFilterButton = Selector('[data-testid=reset-filter-btn]'); clearSelectionButton = Selector('[data-testid=clear-selection-btn]'); - treeViewDelimiterButton = Selector('[data-testid=tree-view-delimiter-btn]'); - treeViewDelimiterValueSave = Selector('[data-testid=apply-btn]'); - treeViewDelimiterValueCancel = Selector('[data-testid=cancel-btn]'); fullScreenModeButton = Selector('[data-testid=toggle-full-screen]'); closeRightPanel = Selector('[data-testid=close-right-panel-btn]'); addNewStreamEntry = Selector('[data-testid=add-key-value-items-btn]'); @@ -177,7 +174,6 @@ export class BrowserPage extends InstancePage { jsonKeyInput = Selector('[data-testid=json-key]'); jsonValueInput = Selector('[data-testid=json-value]'); countInput = Selector('[data-testid=count-input]'); - treeViewDelimiterInput = Selector('[data-testid=tree-view-delimiter-input]'); streamEntryId = Selector('[data-testid=entryId]'); streamField = Selector('[data-testid=field-name]'); streamValue = Selector('[data-testid=field-value]'); @@ -212,27 +208,19 @@ export class BrowserPage extends InstancePage { jsonError = Selector('[data-testid=edit-json-error]'); tooltip = Selector('[role=tooltip]'); noResultsFound = Selector('[data-test-subj=no-result-found]'); + noResultsFoundOnly = Selector('[data-testid=no-result-found-only]'); searchAdvices = Selector('[data-test-subj=search-advices]'); keysNumberOfResults = Selector('[data-testid=keys-number-of-results]'); keysTotalNumber = Selector('[data-testid=keys-total]'); overviewConnectedClients = Selector('[data-test-subj=overview-connected-clients]'); overviewCommandsSec = Selector('[data-test-subj=overview-commands-sec]'); overviewCpu = Selector('[data-test-subj=overview-cpu]'); - treeViewArea = Selector('[data-test-subj=tree-view-panel]'); scannedValue = Selector('[data-testid=keys-number-of-scanned]'); - treeViewKeysNumber = Selector('[data-testid^=count_]'); - treeViewPercentage = Selector('[data-testid^=percentage_]'); - treeViewFolders = Selector('[data-test-subj^=node-arrow-icon_]'); totalKeysNumber = Selector('[data-testid=keys-total]'); databaseInfoToolTip = Selector('[data-testid=db-info-tooltip]'); - treeViewDeviceFolder = Selector('[data-testid^=node-item_device] div'); - treeViewDeviceKyesCount = Selector('[data-testid^=count_device] span'); ttlValueInKeysTable = Selector('[data-testid^=ttl-]'); stringKeyValue = Selector('.key-details-body pre'); keyDetailsBadge = Selector('.key-details-header .euiBadge__text'); - treeViewKeysItem = Selector('[data-testid*="keys:keys:"]'); - treeViewNotPatternedKeys = Selector('[data-testid*="node-item_keys"]'); - treeViewNodeArrowIcon = Selector('[data-test-subj^=node-arrow-icon_]'); modulesTypeDetails = Selector('[data-testid=modules-type-details]'); filteringLabel = Selector('[data-testid^=badge-]'); keysSummary = Selector('[data-testid=keys-summary]'); @@ -590,6 +578,7 @@ export class BrowserPage extends InstancePage { /** * Delete Key By name after Hovering + * @param keyName The name of the key */ async deleteKeyByNameFromList(keyName: string): Promise { await this.searchByKeyName(keyName); @@ -766,7 +755,10 @@ export class BrowserPage extends InstancePage { .click(this.saveMemberButton); } - //Open key details + /** + * Open key details with search + * @param keyName The name of the key + */ async openKeyDetails(keyName: string): Promise { await this.searchByKeyName(keyName); await t.click(this.keyNameInTheList); @@ -878,66 +870,6 @@ export class BrowserPage extends InstancePage { await t.typeText(this.jsonValueInput, jsonStructure, { replace: true, paste: true }); await t.click(this.applyEditButton); } - /** - * Check tree view structure - * @folders name of folders for tree view build - * @delimiter string with delimiter value - * @commonKeyFolder flag if not patterned keys will be displayed - */ - async checkTreeViewFoldersStructure(folders: string[][], delimiter: string, commonKeyFolder: boolean): Promise { - // Verify that all keys that are not inside of tree view doesn't contain delimiter - if (commonKeyFolder) { - await t - .expect(this.treeViewNotPatternedKeys.exists).ok('Folder with not patterned keys') - .click(this.treeViewNotPatternedKeys); - const notPatternedKeys = Selector('[data-test-subj=key-list-panel]').find(this.cssSelectorKey); - const notPatternedKeysNumber = await notPatternedKeys.count; - for (let i = 0; i < notPatternedKeysNumber; i++) { - await t.expect(notPatternedKeys.nth(i).withText(delimiter).exists).notOk('Not contained delimiter keys'); - } - } - // Verify that every level of tree view is clickable - const foldersNumber = folders.length; - for (let i = 0; i < foldersNumber; i++) { - const innerFoldersNumber = folders[i].length; - const array: string[] = []; - for (let j = 0; j < innerFoldersNumber; j++) { - if (j === 0) { - const folderSelector = `[data-testid="node-item_${folders[i][j]}${delimiter}"]`; - array.push(folderSelector); - await t.click(Selector(folderSelector)); - } - else { - const lastSelector = array[array.length - 1].substring(0, array[array.length - 1].length - 2); - const folderSelector = `${lastSelector}${folders[i][j]}${delimiter}"]`; - array.push(folderSelector); - await t.click(Selector(folderSelector)); - } - } - // Verify that the last folder level contains required keys - const lastSelector = array[array.length - 1].substring(0, array[array.length - 1].length - 2); - const folderSelector = `${lastSelector}keys${delimiter}keys${delimiter}"]`; - await t.click(Selector(folderSelector)); - const foundKeyName = `${folders[i].join(delimiter)}`; - await t - .expect(Selector(`[data-testid*="key-${foundKeyName}"]`).exists).ok('Specific key not found') - .click(array[0]); - } - } - - /** - * Change delimiter value - * @delimiter string with delimiter value - */ - async changeDelimiterInTreeView(delimiter: string): Promise { - // Open delimiter popup - await t.click(this.treeViewDelimiterButton); - // Apply new value to the field - await t.typeText(this.treeViewDelimiterInput, delimiter, { replace: true, paste: true }); - // Click on save button - await t.click(this.treeViewDelimiterValueSave); - await t.expect(this.treeViewDelimiterButton.withExactText(delimiter).exists).ok('Delimiter is not changed'); - } //Delete entry from Stream key async deleteStreamEntry(): Promise { @@ -1019,33 +951,6 @@ export class BrowserPage extends InstancePage { .click(option); } - /** - * Get text from first tree element - */ - async getTextFromNthTreeElement(number: number): Promise { - return (await Selector('[role="treeitem"]').nth(number).find('div').textContent).replace(/\s/g, ''); - } - - /** - * Open tree folder with multiple level - * @param names folder names with sequence of subfolder - */ - async openTreeFolders(names: string[]): Promise { - let base = `node-item_${names[0]}:`; - await t.click(Selector(`[data-testid="${base}"]`)); - if (names.length > 1) { - for (let i = 1; i < names.length; i++) { - base = `${base }${names[i]}:`; - await t.click(Selector(`[data-testid="${base}"]`)); - } - } - await t.click(Selector(`[data-testid="${base}keys:keys:"]`)); - - await t.expect( - Selector(`[data-testid="${base}keys:keys:"]`).visible) - .ok('Folder is not selected'); - } - /** * Verify that database has no keys */ @@ -1061,6 +966,10 @@ export class BrowserPage extends InstancePage { await t.click(this.clearFilterButton); } + /** + * Open Guide link by name + * @param guide The guide name + */ async clickGuideLinksByName(guide: string): Promise { const linkGuide = Selector(`[data-testid="guide-button-${guide}"]`); await t.click(linkGuide); diff --git a/tests/e2e/pageObjects/components/browser/index.ts b/tests/e2e/pageObjects/components/browser/index.ts index 8a8463c193..d61d98397b 100644 --- a/tests/e2e/pageObjects/components/browser/index.ts +++ b/tests/e2e/pageObjects/components/browser/index.ts @@ -1,5 +1,7 @@ import { BulkActions } from './bulk-actions'; +import { TreeView } from './tree-view'; export { - BulkActions + BulkActions, + TreeView }; diff --git a/tests/e2e/pageObjects/components/browser/tree-view.ts b/tests/e2e/pageObjects/components/browser/tree-view.ts new file mode 100644 index 0000000000..1a5c7d473c --- /dev/null +++ b/tests/e2e/pageObjects/components/browser/tree-view.ts @@ -0,0 +1,135 @@ +import { Selector, t } from 'testcafe'; +import { Common } from '../../../helpers/common'; + +export class TreeView { + //------------------------------------------------------------------------------------------- + //DECLARATION OF SELECTORS + //*Declare all elements/components of the relevant page. + //*Target any element/component via data-id, if possible! + //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). + //------------------------------------------------------------------------------------------- + //BUTTONS + treeViewSettingsBtn = Selector('[data-testid=tree-view-settings-btn]'); + treeViewDelimiterValueSave = Selector('[data-testid=tree-view-apply-btn]'); + treeViewDelimiterValueCancel = Selector('[data-testid=tree-view-cancel-btn]'); + sortingBtn = Selector('[data-testid=tree-view-sorting-select]'); + sortingASCoption = Selector('[id=ASC]'); + sortingDESCoption = Selector('[id=DESC]'); + sortingProgressBar = Selector('[data-testid=progress-key-tree]'); + // TEXT ELEMENTS + treeViewKeysNumber = Selector('[data-testid^=count_]'); + treeViewDeviceFolder = Selector('[data-testid^=node-item_device] div'); + //INPUTS + treeViewDelimiterInput = Selector('[data-testid=tree-view-delimiter-input]'); + + /** + * Get folder selector by folder name + * @param folderName The name of the folder + */ + getFolderSelectorByName(folderName: string): Selector { + return Selector(`[data-testid^="node-item_${folderName}"]`); + } + + /** + * Get folder counter selector by folder name + * @param folderName The name of the folder + */ + getFolderCountSelectorByName(folderName: string): Selector { + return Selector(`[data-testid^="count_${folderName}"]`); + } + + /** + * Verifying if the Keys are in the List of keys + * @param keyNames The names of the keys + * @param isDisplayed True if keys should be displayed + */ + async verifyFolderDisplayingInTheList(folderName: string, isDisplayed: boolean): Promise { + isDisplayed + ? await t.expect(this.getFolderSelectorByName(folderName).exists).ok(`The folder ${folderName} not found`) + : await t.expect(this.getFolderSelectorByName(folderName).exists).notOk(`The folder ${folderName} found`); + } + + /** + * Change delimiter value + * @delimiter string with delimiter value + */ + async changeDelimiterInTreeView(delimiter: string): Promise { + // Open delimiter popup + await t.click(this.treeViewSettingsBtn); + // Apply new value to the field + await t.typeText(this.treeViewDelimiterInput, delimiter, { replace: true, paste: true }); + // Click on save button + await t.click(this.treeViewDelimiterValueSave); + } + + /** + * Change ordering value + * @param order ASC/DESC ordering for tree view + */ + async changeOrderingInTreeView(order: string): Promise { + // Open settings popup + await t.click(this.treeViewSettingsBtn); + await t.click(this.sortingBtn); + order === 'ASC' + ? await t.click(this.sortingASCoption) + : await t.click(this.sortingDESCoption); + + // Click on save button + await t.click(this.treeViewDelimiterValueSave); + await Common.waitForElementNotVisible(this.sortingProgressBar); + } + + /** + * Get text from tree element by number + * @param number The number of tree folder + */ + async getTextFromNthTreeElement(number: number): Promise { + return (await Selector('[role="treeitem"]').nth(number).find('div').textContent).replace(/\s/g, ''); + } + + /** + * Open tree folder with multiple level + * @param names folder names with sequence of subfolder + */ + async openTreeFolders(names: string[]): Promise { + let base = `node-item_${names[0]}:`; + await this.clickElementIfNotExpanded(base); + if (names.length > 1) { + for (let i = 1; i < names.length; i++) { + base = `${base }${names[i]}:`; + await this.clickElementIfNotExpanded(base); + } + } + } + + /** + * Get all keys from tree view list with order + */ + async getAllItemsArray(): Promise { + const textArray: string[] = []; + const treeViewItemElements = Selector('[role="treeitem"]'); + const itemCount = await treeViewItemElements.count; + + for (let i = 0; i < itemCount; i++) { + const treeItem = treeViewItemElements.nth(i); + const keyItem = treeItem.find('[data-testid^="key-"]'); + if (await keyItem.exists) { + textArray.push(await keyItem.textContent); + } + } + + return textArray; + } + + /** + * click on the folder element if it is not expanded + * @param base the base element + */ + private async clickElementIfNotExpanded(base: string): Promise { + const baseSelector = Selector(`[data-testid^="${base}"]`); + const elementSelector = await baseSelector.getAttribute('data-testid'); + if (!elementSelector?.includes('expanded')) { + await t.click(Selector(`[data-testid^="${base}"]`)); + } + } +} diff --git a/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts b/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts index 1c540c794e..104ba0dc9b 100644 --- a/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts +++ b/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts @@ -266,9 +266,8 @@ export class AddRedisDatabase { /** * set copressor value in dropdown * @param compressor - compressor value - * @param value - checkbox value */ - async setCompressorValue(compressor: string){ + async setCompressorValue(compressor: string): Promise { if(!await this.selectCompressor.exists) { await t.click(this.dataCompressorLabel); diff --git a/tests/e2e/pageObjects/index.ts b/tests/e2e/pageObjects/index.ts index 855275febb..5d62c57a5c 100644 --- a/tests/e2e/pageObjects/index.ts +++ b/tests/e2e/pageObjects/index.ts @@ -27,5 +27,5 @@ export { InstancePage, TriggersAndFunctionsLibrariesPage, TriggersAndFunctionsFunctionsPage, - WelcomePage, + WelcomePage }; diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index c94303a85d..d04303adc6 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -290,6 +290,6 @@ export class WorkbenchPage extends InstancePage { do { imageHeight = await selector.getStyleProperty('height'); } - while ((imageHeight == '0px') && Date.now() - startTime < searchTimeout); + while ((imageHeight === '0px') && Date.now() - startTime < searchTimeout); } } diff --git a/tests/e2e/tests/electron/critical-path/browser/bulk-upload.e2e.ts b/tests/e2e/tests/electron/critical-path/browser/bulk-upload.e2e.ts index a86c1d2706..11cb0b4bf3 100644 --- a/tests/e2e/tests/electron/critical-path/browser/bulk-upload.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/browser/bulk-upload.e2e.ts @@ -5,7 +5,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { deleteAllKeysFromDB, verifyKeysDisplayedInTheList } from '../../../../helpers/keys'; +import { deleteAllKeysFromDB, verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); @@ -53,7 +53,7 @@ test('Verify bulk upload of different text docs formats', async t => { await browserPage.BulkActions.uploadFileInBulk(filePathes.allKeysFile); await verifyCompletedResultText(allKeysResults); await browserPage.searchByKeyName('*key1'); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); // Verify that Upload button disabled after starting new upload await t.click(browserPage.BulkActions.bulkActionStartNewButton); @@ -61,8 +61,7 @@ test('Verify bulk upload of different text docs formats', async t => { // Verify that user can remove uploaded file await t.setFilesToUpload(browserPage.BulkActions.bulkUploadInput, [filePathes.bigDataFile]); - // update after resolving testcafe Native Automation mode limitations - // await t.expect(browserPage.BulkActions.bulkUploadContainer.textContent).contains(filesToUpload[1], 'Filename not displayed in upload input'); + await t.expect(browserPage.BulkActions.bulkUploadContainer.textContent).contains(filesToUpload[1], 'Filename not displayed in upload input'); await t.click(browserPage.BulkActions.removeFileBtn); await t.expect(browserPage.BulkActions.bulkUploadContainer.textContent).contains(defaultText, 'File not removed from upload input'); diff --git a/tests/e2e/tests/electron/critical-path/database/add-ssh-db.e2e.ts b/tests/e2e/tests/electron/critical-path/database/add-ssh-db.e2e.ts index e5bfba5778..b4f84e9ced 100644 --- a/tests/e2e/tests/electron/critical-path/database/add-ssh-db.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/database/add-ssh-db.e2e.ts @@ -5,13 +5,13 @@ 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'; +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 browserActions = new BrowserActions(); const sshParams = { sshHost: '172.31.100.245', @@ -43,6 +43,12 @@ fixture `Adding database with SSH` await databaseAPIRequests.deleteStandaloneDatabasesByNamesApi([sshDbPass.databaseName, sshDbPrivateKey.databaseName, sshDbPasscode.databaseName, newClonedDatabaseAlias]); }); test('Adding database with SSH', async t => { + const tooltipText = [ + 'Enter a value for required fields (3):', + 'SSH Host', + 'SSH Username', + 'SSH Private Key' + ]; const hiddenPass = '••••••••••••'; const sshWithPass = { ...sshParams, @@ -57,25 +63,24 @@ test('Adding database with SSH', async t => { sshPrivateKey: sshPrivateKeyWithPasscode, sshPassphrase: 'test' }; - // update after resolving testcafe Native Automation mode limitations - // // Verify that if user have not entered any required value he can see that this field should be specified when hover over the button to add a database - // await t - // .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseButton) - // .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseManually) - // .click(myRedisDatabasePage.AddRedisDatabase.useSSHCheckbox) - // .click(myRedisDatabasePage.AddRedisDatabase.sshPrivateKeyRadioBtn) - // .hover(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); - // for (const text of tooltipText) { - // await browserActions.verifyTooltipContainsText(text, true); - // } - // // Verify that user can see the Test Connection button enabled/disabled with the same rules as the button to add/apply the changes - // await t.hover(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); - // for (const text of tooltipText) { - // await browserActions.verifyTooltipContainsText(text, true); - // } + // Verify that if user have not entered any required value he can see that this field should be specified when hover over the button to add a database + await t + .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseButton) + .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseManually) + .click(myRedisDatabasePage.AddRedisDatabase.useSSHCheckbox) + .click(myRedisDatabasePage.AddRedisDatabase.sshPrivateKeyRadioBtn) + .hover(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); + for (const text of tooltipText) { + await browserActions.verifyTooltipContainsText(text, true); + } + // Verify that user can see the Test Connection button enabled/disabled with the same rules as the button to add/apply the changes + await t.hover(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); + for (const text of tooltipText) { + await browserActions.verifyTooltipContainsText(text, true); + } // Verify that user can add SSH tunnel with Password for Standalone database - // await t.click(myRedisDatabasePage.AddRedisDatabase.cancelButton); + await t.click(myRedisDatabasePage.AddRedisDatabase.cancelButton); await myRedisDatabasePage.AddRedisDatabase.addStandaloneSSHDatabase(sshDbPass, sshWithPass); await myRedisDatabasePage.clickOnDBByName(sshDbPass.databaseName); await Common.checkURLContainsText('browser'); diff --git a/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts index f3db07e435..1daae0eb76 100644 --- a/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -1,17 +1,15 @@ -import { Selector, t } from 'testcafe'; +import { t } from 'testcafe'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { KeyTypesTexts, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; -import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList } from '../../../../helpers/keys'; -import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); -const apiKeyRequests = new APIKeyRequests(); let keyNames: string[]; let keyName1: string; @@ -27,12 +25,11 @@ test await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { - await t.click(browserPage.patternModeBtn); - await browserPage.deleteKeysByNames(keyNames); + await browserPage.Cli.sendCommandInCli('flushdb'); 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 + keyName1 = Common.generateWord(10); + keyName2 = Common.generateWord(10); keyNameSingle = Common.generateWord(10); keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle]; @@ -41,7 +38,7 @@ test `HSET ${keyNames[0]} field value`, `HSET ${keyNames[1]} field value`, `HSET ${keyNames[2]} field value`, - `HSET ${keyNames[3]} field value`, + `SADD ${keyNames[3]} value`, `SADD ${keyNames[4]} value` ]; @@ -49,17 +46,17 @@ test await browserPage.Cli.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); - await browserPage.openTreeFolders([await browserPage.getTextFromNthTreeElement(1)]); + await browserPage.TreeView.openTreeFolders([await browserPage.TreeView.getTextFromNthTreeElement(1)]); await browserPage.selectFilterGroupType(KeyTypesTexts.Set); // The folder without any namespaces is selected (if exists) when folder does not exist after search/filter - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); await browserPage.setAllKeyType(); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); - await verifyKeysNotDisplayedInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`], false); // switch between browser view and tree view await t.click(browserPage.browserViewButton) @@ -67,57 +64,58 @@ test await browserPage.deleteKeyByName(keyNames[4]); await t.click(browserPage.clearFilterButton); // get first folder name - const firstTreeItemText = await browserPage.getTextFromNthTreeElement(0); - const firstTreeItemKeys = Selector(`[data-testid="node-item_${firstTreeItemText}:keys:keys:"]`); // keys after node item opened - // The first folder with namespaces is expanded and selected when there is no folder without any patterns - await t.expect(firstTreeItemKeys.visible) - .ok('First folder is not expanded'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + const firstTreeItemText = await browserPage.TreeView.getTextFromNthTreeElement(0); + // All folders with namespaces are collapsed when there is no folder without any patterns + await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false); const commands1 = [ - `HSET ${keyNames[4]} field value` + `SADD ${keyNames[4]} value` ]; await browserPage.Cli.sendCommandsInCli(commands1); await t.click(browserPage.refreshKeysButton); - // Refreshed Tree view preselected folder - await t.expect(firstTreeItemKeys.visible) - .ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // Folders are collapsed after refresh + await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false); + await verifyKeysDisplayingInTheList([keyNameSingle], true); await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected after searching with HASH'); - // Filtered Tree view preselected folder - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // Only folders according to key type filter are displayed + await verifyKeysDisplayingInTheList([keyNameSingle], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(firstTreeItemText, true); - await browserPage.searchByKeyName('*'); - // Search capability Filtered Tree view preselected folder - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + await browserPage.searchByKeyName(`${keyName1}*`); + // Only folders according to filter by key names are displayed + await verifyKeysDisplayingInTheList([keyNameSingle], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); await t.click(browserPage.clearFilterButton); - // Filtered Tree view preselected folder - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // All folders are displayed and collapsed after cleared filter + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([keyName1], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); - // Filtered Tree view preselected folder - await t.expect(browserPage.keyListTable.textContent).contains('No results found.', 'Key is not found message not displayed'); + // Verify that No results found message is displayed in case of invalid filtering + await t.expect(browserPage.noResultsFoundOnly.textContent).contains('No results found.', 'Key is not found message not displayed'); await browserPage.setAllKeyType(); // clear stream from filter - // 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.exists) - .notOk('First folder is expanded'); + // Verify that no results found message not displayed after clearing filter + await t.expect(browserPage.noResultsFoundOnly.exists).notOk('Key is not found message still displayed'); + // All folders are displayed and collapsed after cleared filter + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([keyName1], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); }); - test .before(async() => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${index}`); + await browserPage.Cli.sendCommandInCli('flushdb'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify tree view navigation for index based search', async t => { keyName1 = Common.generateWord(10); // used to create index name @@ -139,13 +137,10 @@ test await t.click(browserPage.redisearchModeBtn); // click redisearch button await browserPage.selectIndexByName(index); await t.click(browserPage.treeViewButton); - await t.click(Selector(`[data-testid="${`node-item_${folders[0]}:`}"]`)); // close folder - await browserPage.openTreeFolders(folders); + await browserPage.TreeView.openTreeFolders(folders); await t.click(browserPage.refreshKeysButton); // Refreshed Tree view preselected folder for index based search - await t.expect( - Selector(`[data-testid="node-item_${folders[0]}:${folders[1]}:keys:keys:"]`).visible) - .ok('Folder is not selected'); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); }); test @@ -154,9 +149,7 @@ test }) .after(async() => { await t.click(browserPage.patternModeBtn); - for (const element of keyNames.slice(1)) { - await apiKeyRequests.deleteKeyByNameApi(element, ossStandaloneConfig.databaseName); - } + await browserPage.Cli.sendCommandInCli('flushdb'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Search capability Refreshed Tree view preselected folder', async t => { keyName1 = Common.generateWord(10); @@ -174,24 +167,30 @@ test await browserPage.Cli.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); - await browserPage.openTreeFolders([keyName1]); // Type: hash - await browserPage.openTreeFolders([keyName2]); // Type: list + await browserPage.TreeView.openTreeFolders([keyName1]); // Type: hash + await browserPage.TreeView.openTreeFolders([keyName2]); // Type: list await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - // The first folder with namespaces is expanded and selected when folder and folder without any namespaces does not exist after search/filter - await verifyKeysDisplayedInTheList([keyNames[0], keyNames[1]]); + // Only related to key types filter folders are displayed + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[1]], false); await browserPage.setAllKeyType(); await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames[0]}`]); await t.click(browserPage.refreshKeysButton); // refresh keys - // The previously selected folder is preselected when key does not exist after keys refresh - await verifyKeysDisplayedInTheList([keyNames[1]]); - await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); + // Only related to filter folders are displayed when key does not exist after keys refresh + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await verifyKeysDisplayingInTheList([keyNames[4]], true); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false); await browserPage.searchByKeyName('*'); await t.click(browserPage.refreshKeysButton); // Search capability Refreshed Tree view preselected folder - await verifyKeysDisplayedInTheList([keyNames[1]]); - await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await verifyKeysDisplayingInTheList([keyNames[4]], true); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false); }); diff --git a/tests/e2e/tests/web/critical-path/browser/bulk-upload.e2e.ts b/tests/e2e/tests/web/critical-path/browser/bulk-upload.e2e.ts index 1509482f92..b7c7a2826d 100644 --- a/tests/e2e/tests/web/critical-path/browser/bulk-upload.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/bulk-upload.e2e.ts @@ -5,7 +5,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { deleteAllKeysFromDB, verifyKeysDisplayedInTheList } from '../../../../helpers/keys'; +import { deleteAllKeysFromDB, verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); @@ -53,7 +53,7 @@ test('Verify bulk upload of different text docs formats', async t => { await browserPage.BulkActions.uploadFileInBulk(filePathes.allKeysFile); await verifyCompletedResultText(allKeysResults); await browserPage.searchByKeyName('*key1'); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); // Verify that Upload button disabled after starting new upload await t.click(browserPage.BulkActions.bulkActionStartNewButton); @@ -61,8 +61,7 @@ test('Verify bulk upload of different text docs formats', async t => { // Verify that user can remove uploaded file await t.setFilesToUpload(browserPage.BulkActions.bulkUploadInput, [filePathes.bigDataFile]); - // update after resolving testcafe Native Automation mode limitations - // await t.expect(browserPage.BulkActions.bulkUploadContainer.textContent).contains(filesToUpload[1], 'Filename not displayed in upload input'); + await t.expect(browserPage.BulkActions.bulkUploadContainer.textContent).contains(filesToUpload[1], 'Filename not displayed in upload input'); await t.click(browserPage.BulkActions.removeFileBtn); await t.expect(browserPage.BulkActions.bulkUploadContainer.textContent).contains(defaultText, 'File not removed from upload input'); diff --git a/tests/e2e/tests/web/critical-path/browser/context.e2e.ts b/tests/e2e/tests/web/critical-path/browser/context.e2e.ts index 9125b4a017..98be742867 100644 --- a/tests/e2e/tests/web/critical-path/browser/context.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/context.e2e.ts @@ -100,12 +100,15 @@ test const scrollY = 1000; await t.scroll(browserPage.cssSelectorGrid, 0, scrollY); - const keysCount = await browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow).count; - const targetKey = browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow).nth(Math.floor(keysCount / 2)); + const virtualizedTableKeyIndex = browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow).nth(10); + const targetKeyIndex = await virtualizedTableKeyIndex.getAttribute('aria-rowindex'); + const targetKey = browserPage.virtualTableContainer.find(`[aria-rowindex="${targetKeyIndex}"`); const targetKeyName = await targetKey.find(browserPage.cssSelectorKey).innerText; + // Open key details await t.click(targetKey); - await t.expect(await targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); + // Verify that key selected + await t.expect(targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); await t.click(myRedisDatabasePage.NavigationPanel.settingsButton); // Return back to Browser and check key details selected @@ -113,7 +116,7 @@ test // Check Keys details saved await t.expect(browserPage.keyNameFormDetails.innerText).eql(targetKeyName, 'Key details is not saved as context'); // Check Key selected in Key List - await t.expect(await targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); + await t.expect(targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); }); test .after(async() => { @@ -132,13 +135,13 @@ test await t.click(browserPage.Cli.cliCollapseButton); await t.click(browserPage.refreshKeysButton); - const keyList = await browserPage.keyListTable; - const keyListSGrid = await keyList.find(browserPage.cssSelectorGrid); + const keyList = browserPage.keyListTable; + const keyListSGrid = keyList.find(browserPage.cssSelectorGrid); // Scroll key list await t.scroll(keyListSGrid, 0, scrollY); // Find any key from list that is visible - const renderedRows = await keyList.find(browserPage.cssSelectorRows); + const renderedRows = keyList.find(browserPage.cssSelectorRows); const renderedRowsCount = await renderedRows.count; const randomKey = renderedRows.nth(Math.floor((Math.random() * renderedRowsCount))); const randomKeyName = await randomKey.find(browserPage.cssSelectorKey).textContent; diff --git a/tests/e2e/tests/web/critical-path/browser/formatters.e2e.ts b/tests/e2e/tests/web/critical-path/browser/formatters.e2e.ts index d39ca364e1..89ca3e7946 100644 --- a/tests/e2e/tests/web/critical-path/browser/formatters.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/formatters.e2e.ts @@ -57,11 +57,11 @@ formattersHighlightedSet.forEach(formatter => { // Verify that value is formatted and highlighted await t.expect(valueSelector.find(browserPage.cssJsonValue).exists).ok(`${key.textType} Value is not formatted to ${formatter.format}`); // Verify that Hash field is formatted and highlighted for JSON and PHP serialized - if (key.keyName === 'hash') { + if (key.textType === 'Hash') { await t.expect(browserPage.hashField.find(browserPage.cssJsonValue).exists).ok(`Hash field is not formatted to ${formatter.format}`); } // Verify that Stream field is formatted and highlighted for JSON and PHP serialized - if (key.keyName === 'stream') { + if (key.textType === 'Stream') { await t.expect(Selector(browserPage.cssJsonValue).count).eql(2, `Hash field is not formatted to ${formatter.format}`); } } @@ -193,10 +193,10 @@ notEditableFormattersSet.forEach(formatter => { // Verify for Protobuf, Java serialized, Pickle // Verify for Hash, List, ZSet, String keys for (const key of keysData) { - if (key.keyName === 'hash' || key.keyName === 'list' || key.keyName === 'zset' || key.keyName === 'string') { - const editBtn = (key.keyName === 'string') + if (key.textType === 'Hash' || key.textType === 'List' || key.textType === 'String') { + const editBtn = (key.textType === 'String') ? browserPage.editKeyValueButton - : Selector(`[data-testid^=edit-][data-testid*=${key.keyName.split('-')[0]}]`); + : Selector(`[data-testid*=edit-][data-testid*=${key.keyName.split('-')[0]}]`, { timeout: 500 }); await browserPage.openKeyDetailsByKeyName(key.keyName); await browserPage.selectFormatter(formatter.format); // Verify that edit button disabled @@ -206,6 +206,13 @@ notEditableFormattersSet.forEach(formatter => { // Verify tooltip content await t.expect(browserPage.tooltip.textContent).contains('Cannot edit the value in this format', 'Tooltip has wrong text'); } + if (key.textType === 'Sorted Set') { + const editBtn = Selector(`[data-testid*=edit-][data-testid*=${key.keyName.split('-')[0]}]`, { timeout: 500 }); + await browserPage.openKeyDetailsByKeyName(key.keyName); + await browserPage.selectFormatter(formatter.format); + // Verify that edit button enabled for ZSet + await t.expect(editBtn.hasAttribute('disabled')).notOk(`Key ${key.textType} is disabled for ${formatter.format} formatter`); + } } }); }); diff --git a/tests/e2e/tests/web/critical-path/browser/large-data.e2e.ts b/tests/e2e/tests/web/critical-path/browser/large-data.e2e.ts index 308590fb74..8e7f889085 100644 --- a/tests/e2e/tests/web/critical-path/browser/large-data.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/large-data.e2e.ts @@ -103,7 +103,8 @@ test await t.expect(browserPage.tooltip.textContent).eql(disabledFormattersTooltip, 'Edit button tooltip contains invalid message'); // Verify that user can see String key value with only 5000 characters uploaded if length is more than 5000 - await t.expect((await browserPage.stringKeyValueInput.textContent).length).eql(stringKeyParameters.value.length, 'String key > 5000 value is fully loaded by default'); + // Verify that 3 dots after truncated big strings displayed + await t.expect((await browserPage.stringKeyValueInput.textContent).length).eql(stringKeyParameters.value.length + 3, 'String key > 5000 value is fully loaded by default'); await t.click(browserPage.loadAllBtn); // Verify that user can see "Load all" button for String Key with more than 5000 characters and see full value by clicking on it diff --git a/tests/e2e/tests/web/critical-path/browser/scan-keys.e2e.ts b/tests/e2e/tests/web/critical-path/browser/scan-keys.e2e.ts index 3d51c36c0c..286b01f8ee 100644 --- a/tests/e2e/tests/web/critical-path/browser/scan-keys.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/scan-keys.e2e.ts @@ -32,7 +32,10 @@ fixture `Browser - Specify Keys to Scan` .beforeEach(async() => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) - .afterEach(async() => { + .afterEach(async t => { + await t.click(myRedisDatabasePage.NavigationPanel.settingsButton); + await t.click(settingsPage.accordionAdvancedSettings); + await settingsPage.changeKeysToScanValue('10000'); //Clear and delete database await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); diff --git a/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts index ac7d0b952d..6e5d4d9cfb 100644 --- a/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts @@ -10,8 +10,9 @@ import { import { rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; -import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList } from '../../../../helpers/keys'; +import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +import { goBackHistory } from '../../../../helpers/utils'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -75,13 +76,13 @@ test await t.hover(browserPage.redisearchModeBtn); await t.expect(browserPage.tooltip.textContent).contains(redisearchModeTooltipText, 'Invalid text in redisearch mode tooltip'); - // Verify that user see the "Select an index" message when he switch to Search + // Verify that user see the "Select an index" message when he switches to Search await t.click(browserPage.redisearchModeBtn); await t.expect(browserPage.keyListTable.textContent).contains(notSelectedIndexText, 'Select an index message not displayed'); // Verify that user can search by index in Browser view await browserPage.selectIndexByName(indexName); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); await t.expect(browserPage.getKeySelectorByName(keyName).exists).notOk('Key without index displayed after search'); // Verify that user can search by index plus key value await browserPage.searchByKeyName('Hall School'); @@ -107,15 +108,15 @@ test // Verify that user can search by index in Tree view await t.click(browserPage.treeViewButton); // Change delimiter - await browserPage.changeDelimiterInTreeView('-'); + await browserPage.TreeView.changeDelimiterInTreeView('-'); await browserPage.selectIndexByName(indexName); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); await t.expect(browserPage.getKeySelectorByName(keyName).exists).notOk('Key without index displayed after search'); - // Verify that user see the database scanned when he switch to Pattern search mode + // Verify that user see the database scanned when he switches to Pattern search mode await t.click(browserPage.patternModeBtn); await t.click(browserPage.browserViewButton); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); await t.expect(browserPage.getKeySelectorByName(keyName).exists).ok('Database not scanned after returning to Pattern search mode'); }); test @@ -154,16 +155,14 @@ test await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); })('No RediSearch module message', async t => { 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'; + const externalPageLink = 'https://redis.com/try-free/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_browser_search'; await t.click(browserPage.redisearchModeBtn); // Verify that user can see message in the dialog when he doesn't have RediSearch module await t.expect(browserPage.noReadySearchDialogTitle.textContent).contains(noRedisearchMessage, 'Invalid text in no redisearch popover'); - // update after resolving testcafe Native Automation mode limitations - // // Verify that user can navigate by link to create a Redis db - // await t.click(browserPage.redisearchFreeLink); - // await Common.checkURL(externalPageLink); - // await t.switchToParentWindow(); + // Verify that user can navigate by link to create a Redis db + await t.click(browserPage.redisearchFreeLink); + await Common.checkURL(externalPageLink); }); test .before(async() => { @@ -173,7 +172,7 @@ test await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${indexName}`); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Index creation', async t => { - // const createIndexLink = 'https://redis.io/commands/ft.create/'; + const createIndexLink = 'https://redis.io/commands/ft.create/'; // Verify that user can cancel index creation await t.click(browserPage.redisearchModeBtn); @@ -188,13 +187,14 @@ test await t.click(browserPage.selectIndexDdn); await t.click(browserPage.createIndexBtn); await t.expect(browserPage.newIndexPanel.exists).ok('New Index panel is not displayed'); - // update after resolving testcafe Native Automation mode limitations - // // Verify that user can see a link to create a profound index and navigate - // await t.click(browserPage.newIndexPanel.find('a')); - // await Common.checkURL(createIndexLink); - // await t.switchToParentWindow(); + // Verify that user can see a link to create a profound index and navigate + await t.click(browserPage.newIndexPanel.find('a')); + await Common.checkURL(createIndexLink); + await goBackHistory(); // Verify that user can create an index with multiple prefixes + await t.click(browserPage.selectIndexDdn); + await t.click(browserPage.createIndexBtn); await t.click(browserPage.indexNameInput); await t.typeText(browserPage.indexNameInput, indexName); await t.click(browserPage.prefixFieldInput); @@ -273,9 +273,7 @@ test 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 - */ + // Link to ticket: https://redislabs.atlassian.net/browse/RI-3863 // key names to validate in the standalone database keyNames = [`${keyNameSimpleDb}:1`, `${keyNameSimpleDb}:2`, `${keyNameSimpleDb}:3`, `${keyNameSimpleDb}:4`, `${keyNameSimpleDb}:5`]; @@ -308,14 +306,14 @@ test await t.click(browserPage.treeViewButton); // switch to tree view await t.click(browserPage.redisearchModeBtn); // click redisearch button await browserPage.selectIndexByName(indexNameSimpleDb); // select pre-created index in the standalone database - await browserPage.changeDelimiterInTreeView('-'); // change delimiter in tree view to be able to verify keys easily + await browserPage.TreeView.changeDelimiterInTreeView('-'); // change delimiter in tree view to be able to verify keys easily - await verifyKeysDisplayedInTheList(keyNames); // verify created keys are visible + await verifyKeysDisplayingInTheList(keyNames, true); // verify created keys are visible await t.click(browserPage.OverviewPanel.myRedisDBLink); // go back to database selection page await myRedisDatabasePage.clickOnDBByName(bigDbName); // click database name from ossStandaloneBigConfig.databaseName - await verifyKeysNotDisplayedInTheList(keyNames); // Verify that standandalone database keys are NOT visible + await verifyKeysDisplayingInTheList(keyNames, false); // Verify that standandalone database keys are NOT visible await t.expect(Selector('span').withText('Select Index').exists).ok('Index is still selected'); }); diff --git a/tests/e2e/tests/web/critical-path/browser/stream-key.e2e.ts b/tests/e2e/tests/web/critical-path/browser/stream-key.e2e.ts index c2123af937..2771935808 100644 --- a/tests/e2e/tests/web/critical-path/browser/stream-key.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/stream-key.e2e.ts @@ -49,7 +49,7 @@ test('Verify that user can add several fields and values during Stream key creat const streamData = { 'string': Common.generateWord(20), 'array': `[${Common.generateWord(20)}, ${chance.integer()}]`, 'integer': `${chance.integer()}`, 'json': '{\'test\': \'test\'}', 'null': 'null', 'boolean': 'true' }; const scrollSelector = Selector('.eui-yScroll').nth(-1); - // Open Add New Stream Key Form + // Open Add New Stream Key form await browserPage.commonAddNewKey(keyName); await t.click(browserPage.streamOption); // Verify that user can see Entity ID filled by * by default on add Stream key form diff --git a/tests/e2e/tests/web/critical-path/cli/cli-command-helper.e2e.ts b/tests/e2e/tests/web/critical-path/cli/cli-command-helper.e2e.ts index 8c688fd678..51c3f57f2c 100644 --- a/tests/e2e/tests/web/critical-path/cli/cli-command-helper.e2e.ts +++ b/tests/e2e/tests/web/critical-path/cli/cli-command-helper.e2e.ts @@ -90,10 +90,8 @@ test('Verify that user can type TS. in Command helper and see commands from Redi await browserPage.CommandHelper.selectFilterGroupType(COMMAND_GROUP_TIMESERIES); // Search per part of command and check all opened commands await browserPage.CommandHelper.checkSearchedCommandInCommandHelper(commandForSearch, timeSeriesCommands); - // update after resolving testcafe Native Automation mode limitations - // // Check the first command documentation url - // await browserPage.CommandHelper.checkURLCommand(timeSeriesCommands[0], `https://redis.io/commands/${timeSeriesCommands[0].toLowerCase()}/`); - // await t.switchToParentWindow(); + // Check the first command documentation url + await browserPage.CommandHelper.checkURLCommand(timeSeriesCommands[0], `https://redis.io/commands/${timeSeriesCommands[0].toLowerCase()}/`); }); // outdated after https://redislabs.atlassian.net/browse/RI-4608 test.skip('Verify that user can type GRAPH. in Command helper and see auto-suggestions from RedisGraph commands.json', async t => { diff --git a/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts b/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts index 033cb5a237..7f2e706480 100644 --- a/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts @@ -9,7 +9,7 @@ import { } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList, verifySearchFilterValue } from '../../../../helpers/keys'; +import { verifyKeysDisplayingInTheList, verifySearchFilterValue } from '../../../../helpers/keys'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); @@ -48,7 +48,7 @@ fixture `Allow to change database index` }); 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"`; - const rememberedConnectedClients = await browserPage.overviewConnectedClients.textContent; + // const rememberedConnectedClients = await browserPage.overviewConnectedClients.textContent; // Change index to logical db // Verify that database index switcher displayed for Standalone db @@ -70,8 +70,8 @@ test('Switching between indexed databases', async t => { await browserPage.addHashKey(keyNameForSearchInLogicalDb); // Verify that data changed for indexed db on Tree view await t.click(browserPage.treeViewButton); - await verifyKeysDisplayedInTheList([keyNameForSearchInLogicalDb]); - await verifyKeysNotDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb], true); + await verifyKeysDisplayingInTheList(keyNames, false); // Filter by Hash keys and search by key name await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); @@ -81,25 +81,25 @@ test('Switching between indexed databases', async t => { // Verify that search/filter saved after switching index in Browser await verifySearchFilterValue(keyNameForSearchInLogicalDb); - await verifyKeysNotDisplayedInTheList([keyNameForSearchInLogicalDb]); + await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb], false); await t.click(browserPage.browserViewButton); // Change index to logical db await browserPage.OverviewPanel.changeDbIndex(1); await verifySearchFilterValue(keyNameForSearchInLogicalDb); - await verifyKeysDisplayedInTheList([keyNameForSearchInLogicalDb]); + await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb], true); // Return to default database and open search capability await browserPage.OverviewPanel.changeDbIndex(0); await t.click(browserPage.redisearchModeBtn); await browserPage.selectIndexByName(indexName); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); // Change index to logical db await browserPage.OverviewPanel.changeDbIndex(1); // Search by value and return to default database await browserPage.searchByKeyName('Hall School'); await browserPage.OverviewPanel.changeDbIndex(0); // Verify that data changed for indexed db on Search capability page - await verifyKeysDisplayedInTheList([keyNames[0]]); + await verifyKeysDisplayingInTheList([keyNames[0]], true); // Change index to logical db await browserPage.OverviewPanel.changeDbIndex(1); // Verify that search/filter saved after switching index in Search capability @@ -116,14 +116,14 @@ test('Switching between indexed databases', async t => { // Clear filter await t.click(browserPage.clearFilterButton); // Verify that data changed for indexed db on Workbench page (on Search capability page) - await verifyKeysDisplayedInTheList([logicalDbKey]); + await verifyKeysDisplayingInTheList([logicalDbKey], true); await t.click(browserPage.patternModeBtn); // Clear filter await t.click(browserPage.clearFilterButton); // Verify that data changed for indexed db on Workbench page - await verifyKeysDisplayedInTheList([keyNameForSearchInLogicalDb, logicalDbKey]); + await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb, logicalDbKey], true); await browserPage.OverviewPanel.changeDbIndex(0); - await verifyKeysNotDisplayedInTheList([logicalDbKey]); + await verifyKeysDisplayingInTheList([logicalDbKey], false); // Go to Analysis Tools page and create new report await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); diff --git a/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts b/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts index b6bd6cb85f..1a8941ae09 100644 --- a/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts @@ -16,7 +16,6 @@ import { } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; - const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const chance = new Chance(); @@ -47,8 +46,8 @@ test 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 => { - let firstDatabaseModules: string[] = []; - let secondDatabaseModules: string[] = []; + const firstDatabaseModules: string[] = []; + const secondDatabaseModules: string[] = []; //Remember modules await t.click(browserPage.OverviewPanel.overviewMoreInfo); const moduleIcons = Selector('div').find('[data-testid^=Redi]'); diff --git a/tests/e2e/tests/web/critical-path/database/connecting-to-the-db.e2e.ts b/tests/e2e/tests/web/critical-path/database/connecting-to-the-db.e2e.ts index 2190d47830..1418413284 100644 --- a/tests/e2e/tests/web/critical-path/database/connecting-to-the-db.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database/connecting-to-the-db.e2e.ts @@ -5,14 +5,14 @@ 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'; +import { BrowserActions } from '../../../../common-actions/browser-actions'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); const welcomePage = new WelcomePage(); -// const browserActions = new BrowserActions(); +const browserActions = new BrowserActions(); const sshParams = { sshHost: '172.31.100.245', @@ -90,12 +90,12 @@ test await databaseAPIRequests.deleteStandaloneDatabasesByNamesApi([sshDbPass.databaseName, sshDbPrivateKey.databaseName, sshDbPasscode.databaseName, newClonedDatabaseAlias]); })('Adding database with SSH', async t => { const hiddenPass = '••••••••••••'; - // const tooltipText = [ - // 'Enter a value for required fields (3):', - // 'SSH Host', - // 'SSH Username', - // 'SSH Private Key' - // ]; + const tooltipText = [ + 'Enter a value for required fields (3):', + 'SSH Host', + 'SSH Username', + 'SSH Private Key' + ]; const sshWithPass = { ...sshParams, sshPassword: 'pass' @@ -109,25 +109,24 @@ test sshPrivateKey: sshPrivateKeyWithPasscode, sshPassphrase: 'test' }; - // update after resolving testcafe Native Automation mode limitations - // // Verify that if user have not entered any required value he can see that this field should be specified when hover over the button to add a database - // await t - // .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseButton) - // .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseManually) - // .click(myRedisDatabasePage.AddRedisDatabase.useSSHCheckbox) - // .click(myRedisDatabasePage.AddRedisDatabase.sshPrivateKeyRadioBtn) - // .hover(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); - // for (const text of tooltipText) { - // await browserActions.verifyTooltipContainsText(text, true); - // } - // // Verify that user can see the Test Connection button enabled/disabled with the same rules as the button to add/apply the changes - // await t.hover(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); - // for (const text of tooltipText) { - // await browserActions.verifyTooltipContainsText(text, true); - // } + // Verify that if user have not entered any required value he can see that this field should be specified when hover over the button to add a database + await t + .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseButton) + .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseManually) + .click(myRedisDatabasePage.AddRedisDatabase.useSSHCheckbox) + .click(myRedisDatabasePage.AddRedisDatabase.sshPrivateKeyRadioBtn) + .hover(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); + for (const text of tooltipText) { + await browserActions.verifyTooltipContainsText(text, true); + } + // Verify that user can see the Test Connection button enabled/disabled with the same rules as the button to add/apply the changes + await t.hover(myRedisDatabasePage.AddRedisDatabase.testConnectionBtn); + for (const text of tooltipText) { + await browserActions.verifyTooltipContainsText(text, true); + } // Verify that user can add SSH tunnel with Password for Standalone database - // await t.click(myRedisDatabasePage.AddRedisDatabase.cancelButton); + await t.click(myRedisDatabasePage.AddRedisDatabase.cancelButton); await myRedisDatabasePage.AddRedisDatabase.addStandaloneSSHDatabase(sshDbPass, sshWithPass); await myRedisDatabasePage.clickOnDBByName(sshDbPass.databaseName); await Common.checkURLContainsText('browser'); diff --git a/tests/e2e/tests/web/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/web/critical-path/database/import-databases.e2e.ts index 1f111a0fc9..971fa8f00b 100644 --- a/tests/e2e/tests/web/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database/import-databases.e2e.ts @@ -125,8 +125,7 @@ test.before(async() => { await t.click(myRedisDatabasePage.closeDialogBtn); await t.click(myRedisDatabasePage.importDatabasesBtn); await t.setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [rdmData.path]); - // update after resolving testcafe Native Automation mode limitations - // await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(fileNames.rdmFullJson, 'Filename not displayed in import input'); + await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(fileNames.rdmFullJson, 'Filename not displayed in import input'); // Click on remove button await t.click(myRedisDatabasePage.removeImportedFileBtn); await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(defaultText, 'File not removed from import input'); @@ -234,7 +233,7 @@ test('Certificates import with/without path', async t => { }); test('Import SSH parameters', async t => { const sshAgentsResult = 'SSH Agents are not supported'; - const sshPrivateKey = '-----BEGIN OPENSSH PRIVATE KEY-----'; + // const sshPrivateKey = '-----BEGIN OPENSSH PRIVATE KEY-----'; await databasesActions.importDatabase(racompSSHData); // Fully imported table with SSH diff --git a/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts b/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts index ad75b106d5..fc49f6025a 100644 --- a/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts +++ b/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts @@ -113,7 +113,7 @@ test .click(browserPage.treeViewButton) .click(browserPage.clearFilterButton); // Change delimiter - await browserPage.changeDelimiterInTreeView('-'); + await browserPage.TreeView.changeDelimiterInTreeView('-'); // Go to Analysis Tools page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); // Create new report @@ -125,7 +125,7 @@ test // No namespaces message with link await t.click(myRedisDatabasePage.NavigationPanel.browserButton); // Change delimiter to delimiter with no keys - await browserPage.changeDelimiterInTreeView('+'); + await browserPage.TreeView.changeDelimiterInTreeView('+'); // Go to Analysis Tools page and create report await t .click(myRedisDatabasePage.NavigationPanel.analysisPageButton) @@ -135,7 +135,7 @@ test await t.expect(memoryEfficiencyPage.topNamespacesEmptyMessage.textContent).contains(noNamespacesMessage, 'No namespaces message not displayed/correct'); // Verify that user can redirect to Tree view by clicking on button await t.click(memoryEfficiencyPage.treeViewLink); - await t.expect(browserPage.treeViewArea.exists).ok('Tree view not opened'); + await t.expect(browserPage.TreeView.treeViewSettingsBtn.exists).ok('Tree view not opened'); }); test .before(async t => { diff --git a/tests/e2e/tests/web/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/web/critical-path/memory-efficiency/recommendations.e2e.ts index 776c4a7daf..87ff75a84d 100644 --- a/tests/e2e/tests/web/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/web/critical-path/memory-efficiency/recommendations.e2e.ts @@ -39,7 +39,7 @@ fixture `Memory Efficiency Recommendations` // Go to Analysis Tools page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); }) - .afterEach(async t => { + .afterEach(async() => { // Clear and delete database await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); @@ -174,7 +174,7 @@ test.skip // Go to Recommendations tab await t.click(memoryEfficiencyPage.recommendationsTab); }) - .after(async t => { + .after(async() => { // Clear and delete database await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); diff --git a/tests/e2e/tests/web/critical-path/memory-efficiency/top-keys-table.e2e.ts b/tests/e2e/tests/web/critical-path/memory-efficiency/top-keys-table.e2e.ts index 9e417e3ebe..d902c93ba3 100644 --- a/tests/e2e/tests/web/critical-path/memory-efficiency/top-keys-table.e2e.ts +++ b/tests/e2e/tests/web/critical-path/memory-efficiency/top-keys-table.e2e.ts @@ -3,7 +3,7 @@ import { Selector } from 'testcafe'; import { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage } from '../../../../pageObjects'; import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../../helpers/conf'; +import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { deleteAllKeysFromDB, populateDBWithHashes, populateHashWithFields } from '../../../../helpers/keys'; import { Common } from '../../../../helpers/common'; @@ -80,7 +80,7 @@ test // Go to Analysis Tools page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); }) - .after(async t => { + .after(async() => { await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneRedisearch.databaseName); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Big highlighted key tooltip', async t => { diff --git a/tests/e2e/tests/web/critical-path/monitor/save-commands.e2e.ts b/tests/e2e/tests/web/critical-path/monitor/save-commands.e2e.ts index 37fa92cd8d..253e568f8c 100644 --- a/tests/e2e/tests/web/critical-path/monitor/save-commands.e2e.ts +++ b/tests/e2e/tests/web/critical-path/monitor/save-commands.e2e.ts @@ -30,20 +30,19 @@ fixture `Save commands` 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 = [ - // 'Allows you to download the generated log file after pausing the Profiler', - // 'Profiler log is saved to a file on your local machine with no size limitation. The temporary log file will be automatically rewritten when the Profiler is reset.' - // ]; + const toolTip = [ + 'Allows you to download the generated log file after pausing the Profiler', + 'Profiler log is saved to a file on your local machine with no size limitation. The temporary log file will be automatically rewritten when the Profiler is reset.' + ]; await t.click(browserPage.Profiler.expandMonitor); // Check the toggle and Tooltip for Save log await t.expect(browserPage.Profiler.saveLogSwitchButton.exists).ok('The toggle that allows to save Profiler log is not displayed'); - // update after resolving testcafe Native Automation mode limitations - // await t.hover(browserPage.Profiler.saveLogSwitchButton); - // for (const message of toolTip) { - // await t.click(browserPage.Profiler.saveLogSwitchButton); - // await t.expect(browserPage.Profiler.saveLogToolTip.textContent).contains(message, 'The toolTip for save log in Profiler is not displayed'); - // } + await t.hover(browserPage.Profiler.saveLogSwitchButton); + for (const message of toolTip) { + await t.click(browserPage.Profiler.saveLogSwitchButton); + await t.expect(browserPage.Profiler.saveLogToolTip.textContent).contains(message, 'The toolTip for save log in Profiler is not displayed'); + } // Check toggle state await t.expect(browserPage.Profiler.saveLogSwitchButton.getAttribute('aria-checked')).eql('false', 'The toggle state is not OFF when Profiler opened'); }); diff --git a/tests/e2e/tests/web/critical-path/settings/settings.e2e.ts b/tests/e2e/tests/web/critical-path/settings/settings.e2e.ts index fcc26fdc21..f69c7fbcc0 100644 --- a/tests/e2e/tests/web/critical-path/settings/settings.e2e.ts +++ b/tests/e2e/tests/web/critical-path/settings/settings.e2e.ts @@ -21,6 +21,9 @@ fixture `Settings` .clientScripts({ content: `(${explicitErrorHandler.toString()})()` }) .beforeEach(async() => { await databaseHelper.acceptLicenseTerms(); + }) + .afterEach(async() => { + await settingsPage.changeKeysToScanValue('10000'); }); 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/web/critical-path/tree-view/delimiter.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts index 1a800a1ba0..db1e2ffa7c 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts @@ -3,8 +3,10 @@ import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { BrowserActions } from '../../../../common-actions/browser-actions'; const browserPage = new BrowserPage(); +const browserActions = new BrowserActions(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -20,19 +22,20 @@ fixture `Delimiter tests` test('Verify that user can see that input is not saved when the Cancel button is clicked', async t => { // Switch to tree view await t.click(browserPage.treeViewButton); + await t.click(browserPage.TreeView.treeViewSettingsBtn); // Check the default delimiter value - await t.expect(browserPage.treeViewDelimiterButton.withExactText(':').exists).ok('Default delimiter not applied'); - // Open delimiter popup - await t.click(browserPage.treeViewDelimiterButton); + await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'Default delimiter not applied'); // Apply new value to the field - await t.typeText(browserPage.treeViewDelimiterInput, 'test', { replace: true }); + await t.typeText(browserPage.TreeView.treeViewDelimiterInput, 'test', { replace: true }); // Click on Cancel button - await t.click(browserPage.treeViewDelimiterValueCancel); + await t.click(browserPage.TreeView.treeViewDelimiterValueCancel); // Check the previous delimiter value - await t.expect(browserPage.treeViewDelimiterButton.withExactText(':').exists).ok('Previous delimiter not applied'); + await t.click(browserPage.TreeView.treeViewSettingsBtn); + await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'Previous delimiter not applied'); + await t.click(browserPage.TreeView.treeViewDelimiterValueCancel); // Change delimiter - await browserPage.changeDelimiterInTreeView('-'); + await browserPage.TreeView.changeDelimiterInTreeView('-'); // Verify that when user changes the delimiter and clicks on Save button delimiter is applied - await browserPage.checkTreeViewFoldersStructure([['device_us', 'west'], ['mobile_eu', 'central'], ['mobile_us', 'east'], ['user_us', 'west'], ['device_eu', 'central'], ['user_eu', 'central']], '-', true); + await browserActions.checkTreeViewFoldersStructure([['device_us', 'west'], ['mobile_eu', 'central'], ['mobile_us', 'east'], ['user_us', 'west'], ['device_eu', 'central'], ['user_eu', 'central']], '-'); }); diff --git a/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts index f3db07e435..30bad9bec1 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -1,17 +1,15 @@ -import { Selector, t } from 'testcafe'; +import { t } from 'testcafe'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { KeyTypesTexts, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; -import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList } from '../../../../helpers/keys'; -import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); -const apiKeyRequests = new APIKeyRequests(); let keyNames: string[]; let keyName1: string; @@ -27,12 +25,11 @@ test await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { - await t.click(browserPage.patternModeBtn); - await browserPage.deleteKeysByNames(keyNames); + await browserPage.Cli.sendCommandInCli('flushdb'); 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 + keyName1 = Common.generateWord(10); + keyName2 = Common.generateWord(10); keyNameSingle = Common.generateWord(10); keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle]; @@ -41,7 +38,7 @@ test `HSET ${keyNames[0]} field value`, `HSET ${keyNames[1]} field value`, `HSET ${keyNames[2]} field value`, - `HSET ${keyNames[3]} field value`, + `SADD ${keyNames[3]} value`, `SADD ${keyNames[4]} value` ]; @@ -49,17 +46,17 @@ test await browserPage.Cli.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); - await browserPage.openTreeFolders([await browserPage.getTextFromNthTreeElement(1)]); + await browserPage.TreeView.openTreeFolders([await browserPage.TreeView.getTextFromNthTreeElement(1)]); await browserPage.selectFilterGroupType(KeyTypesTexts.Set); // The folder without any namespaces is selected (if exists) when folder does not exist after search/filter - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); await browserPage.setAllKeyType(); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); - await verifyKeysNotDisplayedInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`], false); // switch between browser view and tree view await t.click(browserPage.browserViewButton) @@ -67,57 +64,58 @@ test await browserPage.deleteKeyByName(keyNames[4]); await t.click(browserPage.clearFilterButton); // get first folder name - const firstTreeItemText = await browserPage.getTextFromNthTreeElement(0); - const firstTreeItemKeys = Selector(`[data-testid="node-item_${firstTreeItemText}:keys:keys:"]`); // keys after node item opened - // The first folder with namespaces is expanded and selected when there is no folder without any patterns - await t.expect(firstTreeItemKeys.visible) - .ok('First folder is not expanded'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + const firstTreeItemText = await browserPage.TreeView.getTextFromNthTreeElement(0); + // All folders with namespaces are collapsed when there is no folder without any patterns + await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false); const commands1 = [ - `HSET ${keyNames[4]} field value` + `SADD ${keyNames[4]} value` ]; await browserPage.Cli.sendCommandsInCli(commands1); await t.click(browserPage.refreshKeysButton); - // Refreshed Tree view preselected folder - await t.expect(firstTreeItemKeys.visible) - .ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // Folders are collapsed after refresh + await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false); + await verifyKeysDisplayingInTheList([keyNameSingle], true); await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected after searching with HASH'); - // Filtered Tree view preselected folder - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // Only folders according to key type filter are displayed + await verifyKeysDisplayingInTheList([keyNameSingle], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(firstTreeItemText, true); - await browserPage.searchByKeyName('*'); - // Search capability Filtered Tree view preselected folder - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + await browserPage.searchByKeyName(`${keyName1}*`); + // Only folders according to filter by key names are displayed + await verifyKeysDisplayingInTheList([keyNameSingle], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); await t.click(browserPage.clearFilterButton); - // Filtered Tree view preselected folder - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); + // All folders are displayed and collapsed after cleared filter + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([keyName1], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); - // Filtered Tree view preselected folder - await t.expect(browserPage.keyListTable.textContent).contains('No results found.', 'Key is not found message not displayed'); + // Verify that No results found message is displayed in case of invalid filtering + await t.expect(browserPage.noResultsFoundOnly.textContent).contains('No results found.', 'Key is not found message not displayed'); await browserPage.setAllKeyType(); // clear stream from filter - // 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.exists) - .notOk('First folder is expanded'); + // Verify that no results found message not displayed after clearing filter + await t.expect(browserPage.noResultsFoundOnly.exists).notOk('Key is not found message still displayed'); + // All folders are displayed and collapsed after cleared filter + await verifyKeysDisplayingInTheList([keyNameSingle], true); + await verifyKeysDisplayingInTheList([keyName1], false); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); }); - test .before(async() => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${index}`); + await browserPage.Cli.sendCommandInCli('flushdb'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify tree view navigation for index based search', async t => { keyName1 = Common.generateWord(10); // used to create index name @@ -139,24 +137,18 @@ test await t.click(browserPage.redisearchModeBtn); // click redisearch button await browserPage.selectIndexByName(index); await t.click(browserPage.treeViewButton); - await t.click(Selector(`[data-testid="${`node-item_${folders[0]}:`}"]`)); // close folder - await browserPage.openTreeFolders(folders); + await browserPage.TreeView.openTreeFolders(folders); await t.click(browserPage.refreshKeysButton); // Refreshed Tree view preselected folder for index based search - await t.expect( - Selector(`[data-testid="node-item_${folders[0]}:${folders[1]}:keys:keys:"]`).visible) - .ok('Folder is not selected'); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); }); - test .before(async() => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); }) .after(async() => { await t.click(browserPage.patternModeBtn); - for (const element of keyNames.slice(1)) { - await apiKeyRequests.deleteKeyByNameApi(element, ossStandaloneConfig.databaseName); - } + await browserPage.Cli.sendCommandInCli('flushdb'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Search capability Refreshed Tree view preselected folder', async t => { keyName1 = Common.generateWord(10); @@ -174,24 +166,30 @@ test await browserPage.Cli.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keyNameSingle]); + await verifyKeysDisplayingInTheList([keyNameSingle], true); - await browserPage.openTreeFolders([keyName1]); // Type: hash - await browserPage.openTreeFolders([keyName2]); // Type: list + await browserPage.TreeView.openTreeFolders([keyName1]); // Type: hash + await browserPage.TreeView.openTreeFolders([keyName2]); // Type: list await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - // The first folder with namespaces is expanded and selected when folder and folder without any namespaces does not exist after search/filter - await verifyKeysDisplayedInTheList([keyNames[0], keyNames[1]]); + // Only related to key types filter folders are displayed + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[1]], false); await browserPage.setAllKeyType(); await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames[0]}`]); await t.click(browserPage.refreshKeysButton); // refresh keys - // The previously selected folder is preselected when key does not exist after keys refresh - await verifyKeysDisplayedInTheList([keyNames[1]]); - await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); + // Only related to filter folders are displayed when key does not exist after keys refresh + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await verifyKeysDisplayingInTheList([keyNames[4]], true); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false); await browserPage.searchByKeyName('*'); await t.click(browserPage.refreshKeysButton); // Search capability Refreshed Tree view preselected folder - await verifyKeysDisplayedInTheList([keyNames[1]]); - await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true); + await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true); + await verifyKeysDisplayingInTheList([keyNames[4]], true); + await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false); }); diff --git a/tests/e2e/tests/web/critical-path/tree-view/tree-view.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/tree-view.e2e.ts index 74175a7508..aeab0c37b9 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/tree-view.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/tree-view.e2e.ts @@ -3,16 +3,11 @@ import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; import { rte, KeyTypesTexts } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { Common } from '../../../../helpers/common'; import { verifySearchFilterValue } from '../../../../helpers/keys'; -import { APIKeyRequests } from '../../../../helpers/api/api-keys'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); -const apiKeyRequests = new APIKeyRequests(); - -const keyNameFilter = `keyName${Common.generateWord(10)}`; fixture `Tree view verifications` .meta({ type: 'critical_path', rte: rte.standalone }) @@ -27,35 +22,17 @@ fixture `Tree view verifications` 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) await t.expect(browserPage.browserViewButton.getStyleProperty('background-color')).eql('rgb(41, 47, 71)', 'The Browser is not selected by default'); - await t.expect(browserPage.treeViewArea.exists).notOk('The tree view is displayed', { timeout: 10000 }); + await t.expect(browserPage.TreeView.treeViewSettingsBtn.exists).notOk('The tree view is displayed', { timeout: 5000 }); await t.click(browserPage.treeViewButton); await browserPage.reloadPage(); // Verify that "Tree view" mode enabled state is saved - await t.expect(browserPage.treeViewArea.exists).ok('The tree view is not displayed'); + await t.expect(browserPage.TreeView.treeViewSettingsBtn.exists).ok('The tree view is not displayed'); // Verify that user can scan DB by 10K in tree view await browserPage.verifyScannningMore(); }); -test - .after(async() => { - // Clear and delete database - await apiKeyRequests.deleteKeyByNameApi(keyNameFilter, ossStandaloneBigConfig.databaseName); - 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); - const numberOfKeys = await browserPage.treeViewKeysNumber.textContent; - const percentage = await browserPage.treeViewPercentage.textContent; - // Set filter by key name - await browserPage.searchByKeyName(keyNameFilter); - await t.expect(browserPage.treeViewKeysItem.exists).ok('The key not appeared after the filtering', { timeout: 10000 }); - await t.click(browserPage.treeViewKeysItem); - // Verify the results - await t.expect(browserPage.treeViewKeysNumber.textContent).notEql(numberOfKeys, 'The number of keys is not recalculated'); - await t.expect(browserPage.treeViewPercentage.textContent).notEql(percentage, 'The percentage is not recalculated'); - await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyNameFilter)).ok('The appropriate keys are not displayed'); - }); +// outdated 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 test('Verify that when user switched from Tree View to Browser and goes back state of filer by key name/key type is saved', async t => { const keyName = 'user*'; await t.click(browserPage.treeViewButton); diff --git a/tests/e2e/tests/web/critical-path/url-handling/url-handling.e2e.ts b/tests/e2e/tests/web/critical-path/url-handling/url-handling.e2e.ts index a01a4eaf28..7e2840e65a 100644 --- a/tests/e2e/tests/web/critical-path/url-handling/url-handling.e2e.ts +++ b/tests/e2e/tests/web/critical-path/url-handling/url-handling.e2e.ts @@ -42,6 +42,7 @@ test await t.navigateTo(generateLink(connectUrlParams)); await t.expect(myRedisDatabasePage.AddRedisDatabase.disabledDatabaseInfo.nth(0).getAttribute('title')).contains(host, 'Wrong host value'); await t.expect(myRedisDatabasePage.AddRedisDatabase.disabledDatabaseInfo.nth(1).getAttribute('title')).contains(port, 'Wrong port value'); + await t.wait(5_000); await t.click(myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); // wait for db is added await t.wait(10_000); diff --git a/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts index 288c300b0c..f7db81f308 100644 --- a/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts @@ -10,7 +10,7 @@ const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Cypher syntax at Workbench` - .meta({type: 'critical_path', rte: rte.standalone}) + .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); diff --git a/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts index 271e86e35c..64d8ef1723 100644 --- a/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts @@ -24,7 +24,7 @@ const expectedProperties = [ ]; fixture `Default scripts area at Workbench` - .meta({type: 'critical_path', rte: rte.standalone}) + .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); diff --git a/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts b/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts index 82a11fb17f..318bbcd677 100644 --- a/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts @@ -96,7 +96,8 @@ test await t.click(browserPage.treeViewButton); await browserPage.addHashKey(keyName3); // Verify that user can see Tree view recalculated when new key is added in Tree view - await browserActions.verifyKeyDisplayedTopAndOpened(keyName3); + await browserActions.verifyKeyIsNotDisplayedTop(keyName3); + await t.expect(browserPage.keyNameFormDetails.withExactText(keyName3).exists).ok(`Key ${keyName3} details not opened`); await t.click(browserPage.redisearchModeBtn); await browserPage.selectIndexByName(indexName); diff --git a/tests/e2e/tests/web/regression/browser/filtering.e2e.ts b/tests/e2e/tests/web/regression/browser/filtering.e2e.ts index a637a6e8bc..abc2a24e04 100644 --- a/tests/e2e/tests/web/regression/browser/filtering.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/filtering.e2e.ts @@ -142,10 +142,8 @@ test await browserPage.searchByKeyName(keyName); // Verify that required key is displayed await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('Key not found'); - // Switch to tree view - await t.click(browserPage.treeViewButton); // Check searched key in tree view - await t.click(browserPage.treeViewNotPatternedKeys); + await t.click(browserPage.treeViewButton); await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('Key not found'); }); test diff --git a/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts b/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts index 9f1e3e946f..5f86ecff1c 100644 --- a/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts @@ -7,7 +7,6 @@ import { rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; -import { t } from 'testcafe'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); diff --git a/tests/e2e/tests/web/regression/browser/scan-keys.e2e.ts b/tests/e2e/tests/web/regression/browser/scan-keys.e2e.ts index 647e1195f4..4dea83f750 100644 --- a/tests/e2e/tests/web/regression/browser/scan-keys.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/scan-keys.e2e.ts @@ -16,11 +16,14 @@ const explicitErrorHandler = (): void => { }; fixture `Browser - Specify Keys to Scan` - .meta({type: 'regression', rte: rte.none}) + .meta({ type: 'regression', rte: rte.none }) .page(commonUrl) .clientScripts({ content: `(${explicitErrorHandler.toString()})()` }) .beforeEach(async() => { await databaseHelper.acceptLicenseTerms(); + }) + .afterEach(async() => { + await settingsPage.changeKeysToScanValue('10000'); }); 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/web/regression/browser/survey-link.e2e.ts b/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts index 25ccdd89e7..b33bf81cf5 100644 --- a/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts @@ -2,15 +2,16 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { rte } from '../../../../helpers/constants'; import { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -// import { Common } from '../../../helpers/common'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { Common } from '../../../../helpers/common'; +import { goBackHistory } from '../../../../helpers/utils'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); -// const externalPageLink = 'https://www.surveymonkey.com/r/redisinsight'; +const externalPageLink = 'https://www.surveymonkey.com/r/redisinsight'; fixture `User Survey` .meta({ @@ -25,10 +26,10 @@ test('Verify that user can use survey link', async t => { // Verify that user can see survey link on any page inside of DB // Browser page await t.expect(browserPage.userSurveyLink.visible).ok('Survey Link is not displayed'); - // update after resolving testcafe Native Automation mode limitations - // await t.click(browserPage.userSurveyLink); - // // Verify that when users click on RI survey, they are redirected to https://www.surveymonkey.com/r/redisinsight - // await Common.checkURL(externalPageLink); + await t.click(browserPage.userSurveyLink); + // Verify that when users click on RI survey, they are redirected to https://www.surveymonkey.com/r/redisinsight + await Common.checkURL(externalPageLink); + await goBackHistory(); // await t.switchToParentWindow(); // Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); diff --git a/tests/e2e/tests/web/regression/cli/cli-command-helper.e2e.ts b/tests/e2e/tests/web/regression/cli/cli-command-helper.e2e.ts index 218fc831b8..9aa89d8181 100644 --- a/tests/e2e/tests/web/regression/cli/cli-command-helper.e2e.ts +++ b/tests/e2e/tests/web/regression/cli/cli-command-helper.e2e.ts @@ -4,6 +4,7 @@ import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { BrowserPage } from '../../../../pageObjects'; +import { goBackHistory } from '../../../../helpers/utils'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); @@ -89,12 +90,10 @@ test('Verify that user can see in Command helper and click on new group "JSON", await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandToCheck)); // Verify results of opened command await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandArgumentsToCheck, 'Selected command title not correct'); - // update after resolving testcafe Native Automation mode limitations - // // Click on Read More link for selected command - // await t.click(browserPage.CommandHelper.readMoreButton); - // // Check new opened window page with the correct URL - // await Common.checkURL(externalPageLink); - // await t.switchToParentWindow(); + // Click on Read More link for selected command + await t.click(browserPage.CommandHelper.readMoreButton); + // Check new opened window page with the correct URL + await Common.checkURL(externalPageLink); }); test('Verify that user can see in Command helper and click on new group "Search", can choose it and see list of commands in the group', async t => { filteringGroup = 'Search'; @@ -109,12 +108,10 @@ test('Verify that user can see in Command helper and click on new group "Search" await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandToCheck)); // Verify results of opened command await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandArgumentsToCheck, 'Selected command title not correct'); - // update after resolving testcafe Native Automation mode limitations - // // Click on Read More link for selected command - // await t.click(browserPage.CommandHelper.readMoreButton); - // // Check new opened window page with the correct URL - // await Common.checkURL(externalPageLink); - // await t.switchToParentWindow(); + // Click on Read More link for selected command + await t.click(browserPage.CommandHelper.readMoreButton); + // Check new opened window page with the correct URL + await Common.checkURL(externalPageLink); }); test('Verify that user can see HyperLogLog title in Command Helper for this command group', async t => { filteringGroup = 'HyperLogLog'; @@ -129,13 +126,10 @@ test('Verify that user can see HyperLogLog title in Command Helper for this comm await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandToCheck)); // Verify results of opened command await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandArgumentsToCheck, 'Selected command title not correct'); - // update after resolving testcafe Native Automation mode limitations - // // Click on Read More link for selected command - // await t.click(browserPage.CommandHelper.readMoreButton); - // // Check new opened window page with the correct URL - // await Common.checkURL(externalPageLink); - // // await t.expect(getPageUrl()).eql(externalPageLink, 'The opened page'); - // await t.switchToParentWindow(); + // Click on Read More link for selected command + await t.click(browserPage.CommandHelper.readMoreButton); + // Check new opened window page with the correct URL + await Common.checkURL(externalPageLink); }); test('Verify that user can see all separated groups for AI json file (model, tensor, inference, script)', async t => { filteringGroups = ['Model', 'Script', 'Inference', 'Tensor']; @@ -168,13 +162,13 @@ test('Verify that user can see all separated groups for AI json file (model, ten await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandsToCheck[i])); // Verify results of opened command await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandsArgumentsToCheck[i], 'Selected command title not correct'); - // update after resolving testcafe Native Automation mode limitations - // // Click on Read More link for selected command - // await t.click(browserPage.CommandHelper.readMoreButton); - // // Check new opened window page with the correct URL - // await Common.checkURL(externalPageLinks[i]); - // // Close the window with external link to switch to the application window - // await t.closeWindow(); + // Click on Read More link for selected command + await t.click(browserPage.CommandHelper.readMoreButton); + // Check new opened window page with the correct URL + await Common.checkURL(externalPageLinks[i]); + // Close the window with external link to switch to the application window + await goBackHistory(); + await t.click(browserPage.CommandHelper.expandCommandHelperButton); i++; } }); @@ -192,13 +186,10 @@ test('Verify that user can work with Gears group in Command Helper (RedisGears m await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandToCheck)); // Verify results of opened command await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandArgumentsToCheck, 'Selected command title not correct'); - // update after resolving testcafe Native Automation mode limitations - // // Verify that user can use Read More link for Gears group in Command Helper (RedisGears module) - // await t.click(browserPage.CommandHelper.readMoreButton); - // // Check new opened window page with the correct URL - // await Common.checkURL(externalPageLink); - // // Close the window with external link to switch to the application window - // await t.closeWindow(); + // Verify that user can use Read More link for Gears group in Command Helper (RedisGears module) + await t.click(browserPage.CommandHelper.readMoreButton); + // Check new opened window page with the correct URL + await Common.checkURL(externalPageLink); }); test('Verify that user can work with Bloom groups in Command Helper (RedisBloom module)', async t => { filteringGroups = ['Bloom Filter', 'CMS', 'TDigest', 'TopK', 'Cuckoo Filter']; @@ -234,13 +225,13 @@ test('Verify that user can work with Bloom groups in Command Helper (RedisBloom await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandsToCheck[i])); // Verify results of opened command await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandsArgumentsToCheck[i], 'Selected command title not correct'); - // update after resolving testcafe Native Automation mode limitations - // // Verify that user can use Read More link for Bloom, Cuckoo, CMS, TDigest, TopK groups in Command Helper (RedisBloom module). - // await t.click(browserPage.CommandHelper.readMoreButton); - // // Check new opened window page with the correct URL - // await Common.checkURL(externalPageLinks[i]); - // // Close the window with external link to switch to the application window - // await t.closeWindow(); + // Verify that user can use Read More link for Bloom, Cuckoo, CMS, TDigest, TopK groups in Command Helper (RedisBloom module). + await t.click(browserPage.CommandHelper.readMoreButton); + // Check new opened window page with the correct URL + await Common.checkURL(externalPageLinks[i]); + // Close the window with external link to switch to the application window + await goBackHistory(); + await t.click(browserPage.CommandHelper.expandCommandHelperButton); i++; } }); diff --git a/tests/e2e/tests/web/regression/database/github.e2e.ts b/tests/e2e/tests/web/regression/database/github.e2e.ts index d32ad3ce98..cbb3036095 100644 --- a/tests/e2e/tests/web/regression/database/github.e2e.ts +++ b/tests/e2e/tests/web/regression/database/github.e2e.ts @@ -4,6 +4,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { MyRedisDatabasePage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { Common } from '../../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseHelper = new DatabaseHelper(); @@ -32,9 +33,7 @@ test('Verify that user can work with Github link in the application', async t => // Verify that user can see the icon for GitHub reference at the bottom of the left side bar on the Workbench page await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); await t.expect(myRedisDatabasePage.NavigationPanel.githubButton.visible).ok('Github button'); - // update after resolving testcafe Native Automation mode limitations - // // Verify that when user clicks on Github icon he redirects to the URL: https://github.com/RedisInsight/RedisInsight - // await t.click(myRedisDatabasePage.NavigationPanel.githubButton); - // await t.expect(getPageUrl()).contains('https://github.com/RedisInsight/RedisInsight', 'Link is not correct'); - // await t.switchToParentWindow(); + // Verify that when user clicks on Github icon he redirects to the URL: https://github.com/RedisInsight/RedisInsight + await t.click(myRedisDatabasePage.NavigationPanel.githubButton); + await Common.checkURLContainsText('https://github.com/RedisInsight/RedisInsight'); }); diff --git a/tests/e2e/tests/web/regression/shortcuts/shortcuts.e2e.ts b/tests/e2e/tests/web/regression/shortcuts/shortcuts.e2e.ts index 686b4c8474..a58099ffa2 100644 --- a/tests/e2e/tests/web/regression/shortcuts/shortcuts.e2e.ts +++ b/tests/e2e/tests/web/regression/shortcuts/shortcuts.e2e.ts @@ -3,10 +3,10 @@ import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { MyRedisDatabasePage } from '../../../../pageObjects'; import { commonUrl } from '../../../../helpers/conf'; +import { Common } from '../../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseHelper = new DatabaseHelper(); -// const getPageUrl = ClientFunction(() => window.location.href); fixture `Shortcuts` .meta({ type: 'regression', rte: rte.none }) @@ -31,12 +31,11 @@ test('Verify that user can see a summary of Shortcuts by clicking "Keyboard Shor // Verify that user can close the Shortcuts await t.click(myRedisDatabasePage.ShortcutsPanel.shortcutsCloseButton); await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsPanel.exists).notOk('Shortcuts panel is not displayed'); - // update after resolving testcafe Native Automation mode limitations - // // Click on the Release Notes in Help Center - // await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton); - // await t.click(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterReleaseNotesButton); - // // Verify redirected link opening Release Notes in Help Center - // await t.expect(getPageUrl()).eql(link, 'The Release Notes link not correct'); + // Click on the Release Notes in Help Center + await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton); + await t.click(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterReleaseNotesButton); + // Verify redirected link opening Release Notes in Help Center + await Common.checkURL('https://github.com/RedisInsight/RedisInsight/releases'); }); test('Verify that user can see description of the “up” shortcut in the Help Center > Keyboard Shortcuts > Workbench table', async t => { const description = [ diff --git a/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts b/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts index 59628f00bc..1a34378be8 100644 --- a/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts +++ b/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts @@ -3,15 +3,23 @@ import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig, + ossStandaloneConfig, + ossStandaloneConfigEmpty, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { KeyTypesTexts, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +import { Common } from '../../../../helpers/common'; +import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const apiKeyRequests = new APIKeyRequests(); + +let keyNames: string[] = []; fixture `Tree view verifications` .meta({ type: 'regression', rte: rte.standalone }) @@ -53,24 +61,123 @@ test('Verify that user can see the total number of keys, the number of keys scan await t.expect(browserPage.scanMoreButton.visible).ok('The scan more button is not displayed on the Tree view'); }); test('Verify that when user deletes the key he can see the key is removed from the folder, the number of keys is reduced, the percentage is recalculated', async t => { + const mainFolder = browserPage.TreeView.getFolderSelectorByName('device'); // Open the first key in the tree view and remove await t.click(browserPage.treeViewButton); - // Verify the default separator - await t.expect(browserPage.treeViewSeparator.textContent).eql(':', 'The “:” (colon) not used as a default separator for namespaces'); + await t.click(browserPage.TreeView.treeViewSettingsBtn); + await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'The “:” (colon) not used as a default separator for namespaces'); // Verify that user can see that “:” (colon) used as a default separator for namespaces and see the number of keys found per each namespace - await t.expect(browserPage.treeViewKeysNumber.visible).ok('The user can not see the number of keys'); + await t.expect(browserPage.TreeView.treeViewKeysNumber.visible).ok('The user can not see the number of keys'); - await t.expect(browserPage.treeViewDeviceFolder.visible).ok('The key folder is not displayed', { timeout: 30000 }); - await t.click(browserPage.treeViewDeviceFolder); - const numberOfKeys = await browserPage.treeViewDeviceKyesCount.textContent; - const keyFolder = await browserPage.treeViewDeviceFolder.nth(2).textContent; - await t.click(browserPage.treeViewDeviceFolder.nth(2)); - await t.click(browserPage.treeViewDeviceFolder.nth(5)); + await t.expect(mainFolder.visible).ok('The key folder is not displayed'); + await t.click(mainFolder); + const numberOfKeys = await browserPage.TreeView.getFolderCountSelectorByName('device').textContent; + const targetFolderName = await mainFolder.nth(1).find('[data-testid^=folder-]').textContent; + const targetFolderSelector = browserPage.TreeView.getFolderSelectorByName(`device:${targetFolderName}`); + await t.click(targetFolderSelector); await browserPage.deleteKey(); // Verify the results - await t.expect(browserPage.treeViewDeviceFolder.nth(2).exists).notOk('The previous folder is not closed after removing key folder'); - await t.click(browserPage.treeViewDeviceFolder); - await t.expect(browserPage.treeViewDeviceFolder.nth(2).textContent).notEql(keyFolder, 'The key folder is not removed from the tree view'); - await t.expect(browserPage.treeViewDeviceKyesCount.textContent).notEql(numberOfKeys, 'The number of keys is not recalculated'); + await t.expect(targetFolderSelector.exists).notOk('The previous folder is not closed after removing key folder'); + await t.click(browserPage.TreeView.treeViewDeviceFolder); + await t.expect(mainFolder.nth(1).textContent).notEql(targetFolderName, 'The key folder is not removed from the tree view'); + const actualCount = await browserPage.TreeView.getFolderCountSelectorByName('device').textContent; + await t.expect(+actualCount).lt(+numberOfKeys, 'The number of keys is not recalculated'); }); +test + .before(async() => { + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty); + }) + .after(async() => { + // Clear and delete database + for(const name of keyNames) { + await apiKeyRequests.deleteKeyByNameApi(name, ossStandaloneConfigEmpty.databaseName); + } + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfigEmpty); + })('Verify that if there are keys without namespaces, they are displayed in the root directory after all folders by default in the Tree view', async t => { + keyNames = [ + `atest:a-${Common.generateWord(10)}`, + `atest:z-${Common.generateWord(10)}`, + `ztest:a-${Common.generateWord(10)}`, + `ztest:z-${Common.generateWord(10)}`, + `atest-${Common.generateWord(10)}`, + `ztest-${Common.generateWord(10)}` + ]; + const commands = [ + 'flushdb', + `HSET ${keyNames[0]} field value`, + `HSET ${keyNames[1]} field value`, + `HSET ${keyNames[2]} field value`, + `SADD ${keyNames[3]} value`, + `SADD ${keyNames[4]} value`, + `HSET ${keyNames[5]} field value` + ]; + const expectedSortedByASC = [ + keyNames[0].split(':')[1], + keyNames[1].split(':')[1], + keyNames[2].split(':')[1], + keyNames[3].split(':')[1], + keyNames[4], + keyNames[5] + ]; + const expectedSortedByDESC = [ + keyNames[3].split(':')[1], + keyNames[2].split(':')[1], + keyNames[1].split(':')[1], + keyNames[0].split(':')[1], + keyNames[5], + keyNames[4] + ]; + + // Create 5 keys + await browserPage.Cli.sendCommandsInCli(commands); + await t.click(browserPage.treeViewButton); + + // Verify that if there are keys without namespaces, they are displayed in the root directory after all folders by default in the Tree view + await browserPage.TreeView.openTreeFolders([`${keyNames[0]}`.split(':')[0]]); + await browserPage.TreeView.openTreeFolders([`${keyNames[2]}`.split(':')[0]]); + let actualItemsArray = await browserPage.TreeView.getAllItemsArray(); + // Verify that user can see all folders and keys sorted by name ASC by default + await t.expect(actualItemsArray).eql(expectedSortedByASC); + + // Verify that user can change the sorting ASC-DESC + await browserPage.TreeView.changeOrderingInTreeView('DESC'); + await browserPage.TreeView.openTreeFolders([`${keyNames[2]}`.split(':')[0]]); + await browserPage.TreeView.openTreeFolders([`${keyNames[0]}`.split(':')[0]]); + actualItemsArray = await browserPage.TreeView.getAllItemsArray(); + await t.expect(actualItemsArray).eql(expectedSortedByDESC); + }); + +https://redislabs.atlassian.net/browse/RI-5131 +test + .before(async() => { + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); + }) + .after(async() => { + await browserPage.Cli.sendCommandInCli('flushdb'); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Verify that if filtering results has only 1 folder, the folder will be expanded', async t => { + const name = Common.generateWord(10); + const additionalCharacter = Common.generateWord(1); + const keyName1 = Common.generateWord(3); + const keyName2 = Common.generateWord(3); + keyNames = [`${name}${additionalCharacter}:${keyName1}`, `${name}${additionalCharacter}:${keyName2}`, name]; + + const commands = [ + 'flushdb', + `HSET ${keyNames[0]} field value`, + `HSET ${keyNames[1]} field value`, + `HSET ${keyNames[2]} field value` + ]; + await browserPage.Cli.sendCommandsInCli(commands); + await t.click(browserPage.treeViewButton); + + // Verify if there is only folder, a user can see keys inside + await browserPage.searchByKeyName(`${name}${additionalCharacter}*`); + await verifyKeysDisplayingInTheList([keyName1, keyName2], true); + + // Verify if there are folder and key, a user can't see keys inside the folder + await browserPage.searchByKeyName(`${name}*`); + await verifyKeysDisplayingInTheList([keyName1, keyName2], false); + }); + diff --git a/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts b/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts index 2a1f478576..12a7de4b41 100644 --- a/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts @@ -10,7 +10,7 @@ const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Autocomplete for entered commands` - .meta({type: 'regression', rte: rte.standalone}) + .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); diff --git a/tests/e2e/tests/web/regression/workbench/context.e2e.ts b/tests/e2e/tests/web/regression/workbench/context.e2e.ts index 224070a4b9..3aded27a14 100644 --- a/tests/e2e/tests/web/regression/workbench/context.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/context.e2e.ts @@ -50,7 +50,7 @@ test('Verify that user can see saved CLI size when navigates away to any other p test('Verify that user can see all the information removed when reloads the page', async t => { const command = 'FT._LIST'; // Create context modificaions and navigate to Browser - await t.typeText(workbenchPage.queryInput, command, { replace: true, speed: speed}); + await t.typeText(workbenchPage.queryInput, command, { replace: true, speed: speed }); await t.click(workbenchPage.Cli.cliExpandButton); await t.click(myRedisDatabasePage.NavigationPanel.browserButton); // Open Workbench page and verify context diff --git a/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts b/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts index e65ff793b2..be706fb127 100644 --- a/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts @@ -12,7 +12,7 @@ const databaseAPIRequests = new DatabaseAPIRequests(); const command = 'GRAPH.QUERY graph'; fixture `Cypher syntax at Workbench` - .meta({type: 'regression', rte: rte.standalone}) + .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); diff --git a/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts index c523cf3af4..e0ee1990d4 100644 --- a/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts @@ -10,7 +10,7 @@ const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Default scripts area at Workbench` - .meta({type: 'regression', rte: rte.standalone}) + .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); @@ -20,132 +20,132 @@ fixture `Default scripts area at Workbench` .afterEach(async() => { // Delete database await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); - }) -test('Verify that user can expand/collapse the enablement area', async t => { - // Hover over Enablement area - await t.hover(workbenchPage.preselectArea); - // Collapse the area with default scripts - await t.click(workbenchPage.collapsePreselectAreaButton); - // Validate that Enablement area is not displayed - await t.expect(workbenchPage.preselectArea.visible).notOk('Enablement area is not collapsed'); - // Expand Enablement area - await t.click(workbenchPage.expandPreselectAreaButton); - // Validate that Enablement area is displayed - await t.expect(workbenchPage.preselectArea.visible).ok('Enablement area is not expanded'); }); +test('Verify that user can expand/collapse the enablement area', async t => { + // Hover over Enablement area + await t.hover(workbenchPage.preselectArea); + // Collapse the area with default scripts + await t.click(workbenchPage.collapsePreselectAreaButton); + // Validate that Enablement area is not displayed + await t.expect(workbenchPage.preselectArea.visible).notOk('Enablement area is not collapsed'); + // Expand Enablement area + await t.click(workbenchPage.expandPreselectAreaButton); + // Validate that Enablement area is displayed + await t.expect(workbenchPage.preselectArea.visible).ok('Enablement area is not expanded'); +}); test('Verify that user can see the [Manual] option in the Enablement area', async t => { - const optionsForCheck = [ - 'Manual', - 'List the Indices', - 'Index info', - 'Search', - 'Aggregate' - ]; + const optionsForCheck = [ + 'Manual', + 'List the Indices', + 'Index info', + 'Search', + 'Aggregate' + ]; - // Remember the options displayed in the area - const countOfOptions = await workbenchPage.preselectButtons.count; - const displayedOptions: string[] = []; - for(let i = 0; i < countOfOptions; i++) { - displayedOptions.push(await workbenchPage.preselectButtons.nth(i).textContent); - } - // Verify the options in the area - for(let i = 0; i < countOfOptions; i++) { - await t.expect(displayedOptions[i]).eql(optionsForCheck[i], `Option ${optionsForCheck} is not in the Enablement area`); - } - }); + // Remember the options displayed in the area + const countOfOptions = await workbenchPage.preselectButtons.count; + const displayedOptions: string[] = []; + for(let i = 0; i < countOfOptions; i++) { + displayedOptions.push(await workbenchPage.preselectButtons.nth(i).textContent); + } + // Verify the options in the area + for(let i = 0; i < countOfOptions; i++) { + await t.expect(displayedOptions[i]).eql(optionsForCheck[i], `Option ${optionsForCheck} is not in the Enablement area`); + } +}); test('Verify that user can see saved article in Enablement area when he leaves Workbench page and goes back again', async t => { - await t.click(workbenchPage.documentButtonInQuickGuides); - await t.expect(workbenchPage.internalLinkWorkingWithHashes.visible).ok('The working with hachs link is not visible', { timeout: 5000 }); - // Open Working with Hashes section - await t.click(workbenchPage.internalLinkWorkingWithHashes); - // Check the button from Hash page is visible - await t.expect(workbenchPage.preselectHashCreate.visible).ok('The end of the page is not visible'); - // Go to Browser page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - // Go back to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); - // Verify that the same article is opened in Enablement area - await t.expect(workbenchPage.preselectHashCreate.visible).ok('The end of the page is not visible'); - // Go to list of DBs page - await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); - // Go back to active DB again - await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); - // Check that user is on Workbench page and "Working with Hashes" page is displayed - await t.expect(workbenchPage.preselectHashCreate.visible).ok('The end of the page is not visible'); - }); + await t.click(workbenchPage.documentButtonInQuickGuides); + await t.expect(workbenchPage.internalLinkWorkingWithHashes.visible).ok('The working with hachs link is not visible', { timeout: 5000 }); + // Open Working with Hashes section + await t.click(workbenchPage.internalLinkWorkingWithHashes); + // Check the button from Hash page is visible + await t.expect(workbenchPage.preselectHashCreate.visible).ok('The end of the page is not visible'); + // Go to Browser page + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + // Go back to Workbench page + await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + // Verify that the same article is opened in Enablement area + await t.expect(workbenchPage.preselectHashCreate.visible).ok('The end of the page is not visible'); + // Go to list of DBs page + await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); + // Go back to active DB again + await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); + // Check that user is on Workbench page and "Working with Hashes" page is displayed + await t.expect(workbenchPage.preselectHashCreate.visible).ok('The end of the page is not visible'); +}); //skipped due the issue RI-2384 test.skip('Verify that user can see saved scroll position in Enablement area when he leaves Workbench page and goes back again', async t => { - // Open Working with Hashes section - await t.click(workbenchPage.documentButtonInQuickGuides); - await t.click(workbenchPage.internalLinkWorkingWithHashes); - // Evaluate the last button in Enablement Area - const buttonsQuantity = await workbenchPage.preselectButtons.count; - const lastButton = workbenchPage.preselectButtons.nth(buttonsQuantity - 1); - // Scroll to the very bottom of the page - await t.scrollIntoView(lastButton); - // Check the scroll position - const scrollPosition = await workbenchPage.scrolledEnablementArea.scrollTop; - // Go to Browser page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - // Go back to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); - // Check that scroll position is saved - await t.expect(await workbenchPage.scrolledEnablementArea.scrollTop).eql(scrollPosition, 'The scroll position status is incorrect'); - // Go to list of DBs page - await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); - // Go back to active DB again - await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); - // Check that scroll position is saved - await t.expect(await workbenchPage.scrolledEnablementArea.scrollTop).eql(scrollPosition, 'Scroll position is not correct'); - }); + // Open Working with Hashes section + await t.click(workbenchPage.documentButtonInQuickGuides); + await t.click(workbenchPage.internalLinkWorkingWithHashes); + // Evaluate the last button in Enablement Area + const buttonsQuantity = await workbenchPage.preselectButtons.count; + const lastButton = workbenchPage.preselectButtons.nth(buttonsQuantity - 1); + // Scroll to the very bottom of the page + await t.scrollIntoView(lastButton); + // Check the scroll position + const scrollPosition = await workbenchPage.scrolledEnablementArea.scrollTop; + // Go to Browser page + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + // Go back to Workbench page + await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + // Check that scroll position is saved + await t.expect(await workbenchPage.scrolledEnablementArea.scrollTop).eql(scrollPosition, 'The scroll position status is incorrect'); + // Go to list of DBs page + await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); + // Go back to active DB again + await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); + // Check that scroll position is saved + await t.expect(await workbenchPage.scrolledEnablementArea.scrollTop).eql(scrollPosition, 'Scroll position is not correct'); +}); test('Verify that user can see the siblings menu by clicking on page counter element between Back and Next buttons', async t => { - const popoverButtons = [ - 'Introduction', - 'Working with Hashes', - 'Working with JSON', - 'Learn More' - ] + const popoverButtons = [ + 'Introduction', + 'Working with Hashes', + 'Working with JSON', + 'Learn More' + ]; - // Open Working with Hashes section and click on the on page counter - await t.click(workbenchPage.documentButtonInQuickGuides); - await t.expect(workbenchPage.internalLinkWorkingWithHashes.visible).ok('The working with hachs link is not visible', { timeout: 5000 }); - await t.click(workbenchPage.internalLinkWorkingWithHashes); - // Verify that user can see the quick navigation section to navigate between siblings under the scrolling content - await t.expect(workbenchPage.enablementAreaPagination.visible).ok('The quick navigation section is not displayed'); + // Open Working with Hashes section and click on the on page counter + await t.click(workbenchPage.documentButtonInQuickGuides); + await t.expect(workbenchPage.internalLinkWorkingWithHashes.visible).ok('The working with hachs link is not visible', { timeout: 5000 }); + await t.click(workbenchPage.internalLinkWorkingWithHashes); + // Verify that user can see the quick navigation section to navigate between siblings under the scrolling content + await t.expect(workbenchPage.enablementAreaPagination.visible).ok('The quick navigation section is not displayed'); - await t.click(workbenchPage.enablementAreaPagination); - // Verify the siblings menu - await t.expect(workbenchPage.enablementAreaPaginationPopover.visible).ok('The siblings menu is not displayed'); - const countOfButtons = await workbenchPage.paginationPopoverButtons.count; - for (let i = 0; i < countOfButtons; i++) { - let popoverButton = workbenchPage.paginationPopoverButtons.nth(i); - await t.expect(popoverButton.textContent).eql(popoverButtons[i], `The siblings menu button ${popoverButtons[i]} is not displayed`); - } - }); + await t.click(workbenchPage.enablementAreaPagination); + // Verify the siblings menu + await t.expect(workbenchPage.enablementAreaPaginationPopover.visible).ok('The siblings menu is not displayed'); + const countOfButtons = await workbenchPage.paginationPopoverButtons.count; + for (let i = 0; i < countOfButtons; i++) { + const popoverButton = workbenchPage.paginationPopoverButtons.nth(i); + await t.expect(popoverButton.textContent).eql(popoverButtons[i], `The siblings menu button ${popoverButtons[i]} is not displayed`); + } +}); test('Verify that the same type of content is supported in the “Tutorials” as in the “Quick Guides”', async t => { - const tutorialsContent = [ - 'Working with JSON', - 'Vector Similarity Search', - 'Redis for time series', - 'Probabilistic data structures' - ]; - const command = 'HSET bikes:10000 '; + const tutorialsContent = [ + 'Working with JSON', + 'Vector Similarity Search', + 'Redis for time series', + 'Probabilistic data structures' + ]; + const command = 'HSET bikes:10000 '; - // Verify the redis stack links - await t.click(workbenchPage.redisStackTutorialsButton); - const linksCount = await workbenchPage.redisStackLinks.count; - for(let i = 0; i < linksCount; i++) { - await t.expect(workbenchPage.redisStackLinks.nth(i).textContent).eql(tutorialsContent[i], `The link ${tutorialsContent[i]} is in the Enablement area`); - } - // Verify the load script to Editor - await t.click(workbenchPage.vectorSimilitaritySearchButton); - // Verify that user can see the pagination for redis stack pages in Tutorials - await t.expect(workbenchPage.enablementAreaPagination.visible).ok('The user can not see the pagination for redis stack pages'); - await t.expect(workbenchPage.nextPageButton.visible).ok('The user can not see the next page for redis stack pages'); - await t.expect(workbenchPage.prevPageButton.visible).ok('The user can not see the prev page for redis stack pages'); + // Verify the redis stack links + await t.click(workbenchPage.redisStackTutorialsButton); + const linksCount = await workbenchPage.redisStackLinks.count; + for(let i = 0; i < linksCount; i++) { + await t.expect(workbenchPage.redisStackLinks.nth(i).textContent).eql(tutorialsContent[i], `The link ${tutorialsContent[i]} is in the Enablement area`); + } + // Verify the load script to Editor + await t.click(workbenchPage.vectorSimilitaritySearchButton); + // Verify that user can see the pagination for redis stack pages in Tutorials + await t.expect(workbenchPage.enablementAreaPagination.visible).ok('The user can not see the pagination for redis stack pages'); + await t.expect(workbenchPage.nextPageButton.visible).ok('The user can not see the next page for redis stack pages'); + await t.expect(workbenchPage.prevPageButton.visible).ok('The user can not see the prev page for redis stack pages'); - await t.expect(workbenchPage.queryInputScriptArea.textContent).eql('', 'The editor is not empty'); - await t.click(workbenchPage.hashWithVectorButton); - const editorContent = (await workbenchPage.queryInputScriptArea.textContent).replace(/\s/g, ' ') - await t.expect(editorContent).eql(command, 'The selected command is not in the Editor'); - }); + await t.expect(workbenchPage.queryInputScriptArea.textContent).eql('', 'The editor is not empty'); + await t.click(workbenchPage.hashWithVectorButton); + const editorContent = (await workbenchPage.queryInputScriptArea.textContent).replace(/\s/g, ' '); + await t.expect(editorContent).eql(command, 'The selected command is not in the Editor'); +}); diff --git a/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts b/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts index cecc4d1397..fce78be20c 100644 --- a/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts @@ -11,7 +11,7 @@ const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); fixture `Empty command history in Workbench` - .meta({type: 'regression'}) + .meta({ type: 'regression' }) .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); diff --git a/tests/e2e/tests/web/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/web/regression/workbench/import-tutorials.e2e.ts index d46d3c39d7..6f9e1c5593 100644 --- a/tests/e2e/tests/web/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/import-tutorials.e2e.ts @@ -6,7 +6,7 @@ import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pag import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; -import { deleteAllKeysFromDB, verifyKeysDisplayedInTheList } from '../../../../helpers/keys'; +import { deleteAllKeysFromDB, verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); @@ -158,7 +158,7 @@ test await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); await workbenchPage.deleteTutorialByName(tutorialName); await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).exists) - .notOk(`${tutorialName} tutorial is not deleted`); + .notOk(`${tutorialName} tutorial is not deleted`); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Verify that user can bulk upload data from custom tutorial', async t => { const allKeysResults = ['9Commands Processed', '9Success', '0Errors']; @@ -205,5 +205,5 @@ test await t.click(myRedisDatabasePage.NavigationPanel.browserButton); // Verify that keys of all types can be uploaded await browserPage.searchByKeyName('*key1*'); - await verifyKeysDisplayedInTheList(keyNames); + await verifyKeysDisplayingInTheList(keyNames, true); }); diff --git a/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts index 4360fdd491..71ab05bdbf 100644 --- a/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts @@ -4,6 +4,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { MyRedisDatabasePage, SettingsPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { Common } from '../../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); @@ -11,8 +12,7 @@ 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 externalPageLink = 'https://redis.io/docs/manual/pipelining/'; const pipelineValues = ['-5', '5', '4', '20']; const commandForSend = '100 scan 0 match * count 5000'; @@ -41,11 +41,9 @@ test('Verify that user can see the text in settings for pipeline with link', asy // Verify text in setting for pipeline await t.expect(settingsPage.accordionWorkbenchSettings.textContent).contains(pipelineText, 'Text is incorrect'); - // update after resolving testcafe Native Automation mode limitations - // await t.click(settingsPage.pipelineLink); - // // Check new opened window page with the correct URL - // await t.expect(getPageUrl()).eql(externalPageLink, 'The opened page is incorrect'); - // await t.switchToParentWindow(); + await t.click(settingsPage.pipelineLink); + // Check new opened window page with the correct URL + await Common.checkURL(externalPageLink); }); test.skip('Verify that only chosen in pipeline number of commands is loading at the same time in Workbench', async t => { await settingsPage.changeCommandsInPipeline(pipelineValues[1]); diff --git a/tests/e2e/tests/web/smoke/cli/cli.e2e.ts b/tests/e2e/tests/web/smoke/cli/cli.e2e.ts index a3e37b738c..6edb91366e 100644 --- a/tests/e2e/tests/web/smoke/cli/cli.e2e.ts +++ b/tests/e2e/tests/web/smoke/cli/cli.e2e.ts @@ -5,6 +5,7 @@ import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { Common } from '../../../../helpers/common'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +import { goBackHistory } from '../../../../helpers/utils'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); @@ -74,15 +75,13 @@ test.skip('Verify that user can use unblocking command', async t => { // Verify that user input is blocked await t.expect(browserPage.Cli.cliCommandInput.exists).notOk('Cli input is still shown'); // Create new window to unblock the client - await t - .openWindow(commonUrl) - .maximizeWindow(); + await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); // Open CLI await t.click(browserPage.Cli.cliExpandButton); // Unblock client await t.typeText(browserPage.Cli.cliCommandInput, `client unblock ${clientId}`, { replace: true, paste: true }); await t.pressKey('enter'); - await t.closeWindow(); + await goBackHistory(); await t.expect(browserPage.Cli.cliCommandInput.exists).ok('Cli input is not shown, the client still blocked', { timeout: 10000 }); }); diff --git a/tests/e2e/tests/web/smoke/database/add-db-from-welcome-page.e2e.ts b/tests/e2e/tests/web/smoke/database/add-db-from-welcome-page.e2e.ts index 1535f1338b..77ffaab599 100644 --- a/tests/e2e/tests/web/smoke/database/add-db-from-welcome-page.e2e.ts +++ b/tests/e2e/tests/web/smoke/database/add-db-from-welcome-page.e2e.ts @@ -4,6 +4,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { MyRedisDatabasePage, BrowserPage, WelcomePage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { goBackHistory } from '../../../../helpers/utils'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browsePage = new BrowserPage(); @@ -15,7 +16,7 @@ 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'; const homebrewPage = 'https://developer.redis.com/create/homebrew/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight'; -const promoPage = 'https://redis.com/redis-enterprise-cloud/overview/?utm_source=redisinsight&utm_medium=main&utm_campaign=main'; +const promoPage = 'https://redis.com/cloud/overview/?utm_source=redisinsight&utm_medium=main&utm_campaign=main'; fixture `Add database from welcome page` .meta({ type: 'smoke', rte: rte.standalone }) @@ -36,20 +37,20 @@ test 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 -test.skip('Verify that all the links are valid from Welcome page', async t => { + +test('Verify that all the links are valid from Welcome page', async t => { // Verify build from source link await t.click(welcomePage.buildFromSource); await t.expect(getPageUrl()).eql(sourcePage, 'Build from source link is not valid'); - await t.switchToParentWindow(); + await goBackHistory(); // Verify build from docker link await t.click(welcomePage.buildFromDocker); await t.expect(getPageUrl()).eql(dockerPage, 'Build from docker page is not valid'); - await t.switchToParentWindow(); + await goBackHistory(); // Verify build from homebrew link await t.click(welcomePage.buildFromHomebrew); await t.expect(getPageUrl()).eql(homebrewPage, 'Build from homebrew page is not valid'); - await t.switchToParentWindow(); + await goBackHistory(); // Verify promo button link await t.click(welcomePage.tryRedisCloudBtn); await t.expect(getPageUrl()).eql(promoPage, 'Promotion link is not valid'); diff --git a/tests/e2e/tests/web/smoke/database/autodiscover-db.e2e.ts b/tests/e2e/tests/web/smoke/database/autodiscover-db.e2e.ts index 2c1d0e16a1..8b814104a6 100644 --- a/tests/e2e/tests/web/smoke/database/autodiscover-db.e2e.ts +++ b/tests/e2e/tests/web/smoke/database/autodiscover-db.e2e.ts @@ -47,11 +47,11 @@ test ); await t.click( myRedisDatabasePage.AddRedisDatabase.addRedisDatabaseButton); - await t.expect(autoDiscoverREDatabases.title.withExactText('Redis Enterprise Cloud Subscriptions').exists) + await t.expect(autoDiscoverREDatabases.title.withExactText('Redis Cloud Subscriptions').exists) .ok('Subscriptions list not displayed', { timeout: 120000 }); // Select subscriptions await t.click(myRedisDatabasePage.AddRedisDatabase.selectAllCheckbox); await t.click(myRedisDatabasePage.AddRedisDatabase.showDatabasesButton); - await t.expect(autoDiscoverREDatabases.title.withExactText('Redis Enterprise Cloud Databases').exists) + await t.expect(autoDiscoverREDatabases.title.withExactText('Redis Cloud Databases').exists) .ok('database page is not displayed', { timeout: 120000 }); }); diff --git a/tests/e2e/upload-custom-plugins.sh b/tests/e2e/upload-custom-plugins.sh index f9b9045edc..9b0cefee80 100755 --- a/tests/e2e/upload-custom-plugins.sh +++ b/tests/e2e/upload-custom-plugins.sh @@ -6,8 +6,8 @@ curl --request GET -sL \ echo "Custom plugins archive was downloaded" -mkdir -p .redisinsight-v2 -unzip -o plugins.zip -d ./.redisinsight-v2/plugins +mkdir -p .redisinsight-app +unzip -o plugins.zip -d ./.redisinsight-app/plugins echo "Custom plugins were unarchived" diff --git a/tests/e2e/web.runner.ts b/tests/e2e/web.runner.ts index f82a61baab..e77ccc1030 100644 --- a/tests/e2e/web.runner.ts +++ b/tests/e2e/web.runner.ts @@ -6,16 +6,16 @@ import testcafe from 'testcafe'; return t .createRunner() .compilerOptions({ - "typescript": { + 'typescript': { configPath: 'tsconfig.testcafe.json', experimentalDecorators: true - }}) + } }) .src((process.env.TEST_FILES || 'tests/web/**/*.e2e.ts').split('\n')) .browsers(['chromium:headless --cache --allow-insecure-localhost --ignore-certificate-errors']) .screenshots({ path: 'report/screenshots/', takeOnFails: true, - pathPattern: '${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png', + pathPattern: '${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png' }) .reporter([ 'spec', @@ -39,14 +39,15 @@ import testcafe from 'testcafe'; assertionTimeout: 5000, speed: 1, quarantineMode: { successThreshold: 1, attemptLimit: 3 }, - pageRequestTimeout: 8000 + pageRequestTimeout: 8000, + disableMultipleWindows: true }); }) .then((failedCount) => { process.exit(failedCount); }) .catch((e) => { - console.error(e) + console.error(e); process.exit(1); }); })(); diff --git a/tests/e2e/yarn.lock b/tests/e2e/yarn.lock index 733f67861c..fd6df09eb4 100644 --- a/tests/e2e/yarn.lock +++ b/tests/e2e/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adobe/css-tools@^4.3.0-rc.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" + integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -1110,12 +1115,18 @@ resolved "https://registry.yarnpkg.com/@devexpress/bin-v8-flags-filter/-/bin-v8-flags-filter-1.3.0.tgz#3069f2525c0c5fb940810e9ec10fc592c47552db" integrity sha512-LWLNfYGwVJKYpmHUDoODltnlqxdEAl5Qmw7ha1+TSpsABeF94NKSWkQTTV1TB4CM02j2pZyqn36nHgaFl8z7qw== -"@devexpress/error-stack-parser@^2.0.6": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@devexpress/error-stack-parser/-/error-stack-parser-2.0.6.tgz#a7c32e54583566bc6abf153c32a8b86d87d1e490" - integrity sha512-fneVypElGUH6Be39mlRZeAu00pccTlf4oVuzf9xPJD1cdEqI8NyAiQua/EW7lZdrbMUbgyXcJmfKPefhYius3A== +"@devexpress/callsite-record@^4.1.6": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@devexpress/callsite-record/-/callsite-record-4.1.7.tgz#045ece71df8c574d7adc15c75eeaac6c6ca6c97d" + integrity sha512-qr3VQYc0KopduFkEY6SxaOIi1Xhm0jIWQfrxxMVboI/p2rjF/Mj/iqaiUxQQP6F3ujpW/7l0mzhf17uwcFZhBA== dependencies: - stackframe "^1.1.1" + "@types/lodash" "^4.14.72" + callsite "^1.0.0" + chalk "^2.4.0" + error-stack-parser "^2.1.4" + highlight-es "^1.0.0" + lodash "4.6.1 || ^4.16.1" + pinkie-promise "^2.0.0" "@electron/asar@^3.2.3": version "3.2.4" @@ -1898,19 +1909,6 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" -callsite-record@^4.0.0: - version "4.1.5" - resolved "https://registry.yarnpkg.com/callsite-record/-/callsite-record-4.1.5.tgz#cfccae67dfd29e0e52a17d88517fc7e4e3d3bdb4" - integrity sha512-OqeheDucGKifjQRx524URgV4z4NaKjocGhygTptDea+DLROre4ZEecA4KXDq+P7qlGCohYVNOh3qr+y5XH5Ftg== - dependencies: - "@devexpress/error-stack-parser" "^2.0.6" - "@types/lodash" "^4.14.72" - callsite "^1.0.0" - chalk "^2.4.0" - highlight-es "^1.0.0" - lodash "4.6.1 || ^4.16.1" - pinkie-promise "^2.0.0" - callsite@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" @@ -2422,6 +2420,11 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + env-paths@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -2439,12 +2442,12 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -error-stack-parser@^1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-1.3.6.tgz#e0e73b93e417138d1cd7c0b746b1a4a14854c292" - integrity sha512-xhuSYd8wLgOXwNgjcPeXMPL/IiiA1Huck+OPvClpJViVNNlJVtM41o+1emp7bPvlCJwCatFX2DWc05/DgfbWzA== +error-stack-parser@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== dependencies: - stackframe "^0.3.1" + stackframe "^1.3.4" es-abstract@^1.19.0, es-abstract@^1.20.4: version "1.21.2" @@ -4366,6 +4369,13 @@ parse5@^1.5.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" integrity sha512-w2jx/0tJzvgKwZa58sj2vAYq/S/K1QJfIB3cWYea/Iu1scFPDQQ3IQiVZTHWtRBwAjv2Yd7S/xeZf3XqLDb3bA== +parse5@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -4859,7 +4869,7 @@ sanitize-filename@^1.6.0: dependencies: truncate-utf8-bytes "^1.0.0" -"semver@2 || 3 || 4 || 5", semver@5.5.0, semver@^5.6.0, semver@^6.0.0, semver@^6.3.1, semver@^7.2.1, semver@^7.3.5, semver@^7.5.2: +"semver@2 || 3 || 4 || 5", semver@5.5.0, semver@7.5.3, semver@^6.0.0, semver@^6.3.1, semver@^7.2.1, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -5036,12 +5046,7 @@ ssri@^8.0.0, ssri@^8.0.1: dependencies: minipass "^3.1.1" -stackframe@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-0.3.1.tgz#33aa84f1177a5548c8935533cbfeb3420975f5a4" - integrity sha512-XmoiF4T5nuWEp2x2w92WdGjdHGY/cZa6LIbRsDRQR/Xlk4uW0PAUlH1zJYVffocwKpCdwyuypIp25xsSXEtZHw== - -stackframe@^1.1.1: +stackframe@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== @@ -5221,10 +5226,10 @@ testcafe-browser-provider-electron@0.0.19: promisify-event "^1.0.0" proxyquire "^1.7.10" -testcafe-browser-tools@2.0.25: - version "2.0.25" - resolved "https://registry.yarnpkg.com/testcafe-browser-tools/-/testcafe-browser-tools-2.0.25.tgz#9721f9ba7024a1c95ca4675554cf9477d792edd2" - integrity sha512-LK/ZOJUwnpjdJl131qrBN0toCv2wZj2Elb8UPTU71n9Woq7kZtGine4P5XvvvO7mE8bjBfWJOBW9jRhHxyIWzQ== +testcafe-browser-tools@2.0.26: + version "2.0.26" + resolved "https://registry.yarnpkg.com/testcafe-browser-tools/-/testcafe-browser-tools-2.0.26.tgz#38b5c0c2cd438895de12ee53cae11c64bd33aab9" + integrity sha512-nTKSJhBzn9BmnOs0xVzXMu8dN2Gu13Ca3x3SJr/zF6ZdKjXO82JlbHu55dt5MFoWjzAQmwlqBkSxPaYicsTgUw== dependencies: array-find "^1.0.0" debug "^4.3.1" @@ -5244,7 +5249,38 @@ testcafe-browser-tools@2.0.25: read-file-relative "^1.2.0" which-promise "^1.0.0" -testcafe-hammerhead@31.4.5, testcafe-hammerhead@>=19.4.0: +testcafe-hammerhead@31.6.1: + version "31.6.1" + resolved "https://registry.yarnpkg.com/testcafe-hammerhead/-/testcafe-hammerhead-31.6.1.tgz#181fe81cf10bd43115087d004dc6c2cad0d02f35" + integrity sha512-tMdF183bTL+hMNzIdUUNpg32T2hlwaI9CEXxOJpgg6VnzCpy1RDV5+wcIJB1ywhs6cdd5ltQZuaHrm1tWbyR1A== + dependencies: + "@adobe/css-tools" "^4.3.0-rc.1" + "@electron/asar" "^3.2.3" + acorn-hammerhead "0.6.2" + bowser "1.6.0" + crypto-md5 "^1.0.0" + debug "4.3.1" + esotope-hammerhead "0.6.4" + http-cache-semantics "^4.1.0" + httpntlm "^1.8.10" + iconv-lite "0.5.1" + lodash "^4.17.20" + lru-cache "2.6.3" + match-url-wildcard "0.0.4" + merge-stream "^1.0.1" + mime "~1.4.1" + mustache "^2.1.1" + nanoid "^3.1.12" + os-family "^1.0.0" + parse5 "^7.1.2" + pinkie "2.0.4" + read-file-relative "^1.2.0" + semver "7.5.3" + tough-cookie "4.1.3" + tunnel-agent "0.6.0" + ws "^7.4.6" + +testcafe-hammerhead@>=19.4.0: version "31.4.5" resolved "https://registry.yarnpkg.com/testcafe-hammerhead/-/testcafe-hammerhead-31.4.5.tgz#672ae1ee426f42fcedb8f15c3e8350f198af2670" integrity sha512-L6flQbgQ4IoCqtjjOVPoJsaq6++9/mYKVCDxUjn5FhMyP/P3pfLcpW0h1wV8UiNkcf6WoYVYhwxN/15g5YMgIQ== @@ -5314,12 +5350,7 @@ testcafe-reporter-minimal@^2.2.0: resolved "https://registry.yarnpkg.com/testcafe-reporter-minimal/-/testcafe-reporter-minimal-2.2.0.tgz#d12624bb6f6b98543ca52512b01002cad23b657d" integrity sha512-iUSWI+Z+kVUAsGegMmEXKDiMPZHDxq+smo4utWwc3wI3Tk6jT8PbNvsROQAjwkMKDmnpo6To5vtyvzvK+zKGXA== -testcafe-reporter-spec@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/testcafe-reporter-spec/-/testcafe-reporter-spec-2.1.1.tgz#8156fced0f5132486559ad560bc80676469275ec" - integrity sha512-KO4c4F5pIORaQ1ddWgNDOyN0GiiKFWtjoMYk3VgBiJYcYuk2ZPN1Ewn0KkZsSsL30tOKeQW6jdp/H+7b4rg5+Q== - -testcafe-reporter-spec@^2.2.0: +testcafe-reporter-spec@2.2.0, testcafe-reporter-spec@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/testcafe-reporter-spec/-/testcafe-reporter-spec-2.2.0.tgz#5c17095da6c680702f34bffb8e3d53bf465125ea" integrity sha512-4jUN75Y7eaHQfSjiCLBXt/TvJMW76kBaZGC74sq03FJNBLoo8ibkEFzfjDJzNDCRYo+P7FjCx3vxGrzgfQU26w== @@ -5339,10 +5370,10 @@ testcafe-selector-generator@^0.1.0: resolved "https://registry.yarnpkg.com/testcafe-selector-generator/-/testcafe-selector-generator-0.1.0.tgz#852c86f71565e5d9320da625c2260d040cbed786" integrity sha512-MTw+RigHsEYmFgzUFNErDxui1nTYUk6nm2bmfacQiKPdhJ9AHW/wue4J/l44mhN8x3E8NgOUkHHOI+1TDFXiLQ== -testcafe@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/testcafe/-/testcafe-3.0.0.tgz#c1be0fe770794cfc773651c40e23ed0096db633d" - integrity sha512-znaK7JQ2kGdEJq2kDmI5sP6QDDkaqwGCxBVYEBOWRr7lUj/bAyFj6x3n3n1WoA3JcwhkRrxJQXQSk/QDgKCCUw== +testcafe@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/testcafe/-/testcafe-3.3.0.tgz#1446b1acca98b1e43c6843c49fce179c38754af4" + integrity sha512-fffFnuZwAlZ4y9mkjygHZUGXad/7pXuj/RHkuwvft0GjRDqRHIcfR5aWrVLGnGpFeO55l48Z2kq1SxT2I+879g== dependencies: "@babel/core" "^7.12.1" "@babel/plugin-proposal-async-generator-functions" "^7.12.1" @@ -5361,13 +5392,13 @@ testcafe@3.0.0: "@babel/preset-react" "^7.12.1" "@babel/runtime" "^7.12.5" "@devexpress/bin-v8-flags-filter" "^1.3.0" + "@devexpress/callsite-record" "^4.1.6" "@types/node" "^12.20.10" async-exit-hook "^1.1.2" babel-plugin-module-resolver "^5.0.0" babel-plugin-syntax-trailing-function-commas "^6.22.0" bowser "^2.8.1" callsite "^1.0.0" - callsite-record "^4.0.0" chai "4.3.4" chalk "^2.3.0" chrome-remote-interface "^0.32.2" @@ -5382,7 +5413,7 @@ testcafe@3.0.0: email-validator "^2.0.4" emittery "^0.4.1" endpoint-utils "^1.0.2" - error-stack-parser "^1.3.6" + error-stack-parser "^2.1.4" execa "^4.0.3" get-os-info "^1.0.2" globby "^11.0.4" @@ -5420,12 +5451,12 @@ testcafe@3.0.0: resolve-cwd "^1.0.0" resolve-from "^4.0.0" sanitize-filename "^1.6.0" - semver "^5.6.0" + semver "^7.5.3" set-cookie-parser "^2.5.1" source-map-support "^0.5.16" strip-bom "^2.0.0" - testcafe-browser-tools "2.0.25" - testcafe-hammerhead "31.4.5" + testcafe-browser-tools "2.0.26" + testcafe-hammerhead "31.6.1" testcafe-legacy-api "5.1.6" testcafe-reporter-json "^2.1.0" testcafe-reporter-list "^2.2.0" @@ -5470,7 +5501,16 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tough-cookie@4.0.0, tough-cookie@^4.1.3: +tough-cookie@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.1.2" + +tough-cookie@4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== @@ -5638,6 +5678,11 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +universalify@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -5811,7 +5856,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@^7.2.0: +ws@^7.2.0, ws@^7.4.6: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== diff --git a/yarn.lock b/yarn.lock index 53b28f336a..5f279b5887 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4662,10 +4662,10 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -ci-info@^3.2.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" - integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +ci-info@^3.2.0, ci-info@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" @@ -7232,6 +7232,13 @@ find-versions@^4.0.0: dependencies: semver-regex "^3.1.2" +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -7657,7 +7664,7 @@ got@^11.7.0, got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -8758,7 +8765,7 @@ is-word-character@^1.0.0: resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== -is-wsl@^2.2.0: +is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -9423,6 +9430,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stable-stringify@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz#e06f23128e0bbe342dc996ed5a19e28b57b580e0" + integrity sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g== + dependencies: + jsonify "^0.0.1" + json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -9456,6 +9470,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + jsonpath@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" @@ -9522,6 +9541,13 @@ kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -11149,6 +11175,14 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + open@^8.0.9: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" @@ -11386,6 +11420,27 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" +patch-package@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61" + integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^9.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + rimraf "^2.6.3" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.0.33" + yaml "^2.2.2" + path-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" @@ -11742,7 +11797,7 @@ postcss-reduce-transforms@^5.1.0: dependencies: postcss-value-parser "^4.2.0" -postcss-selector-parser@^6.0.2: +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: version "6.0.13" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== @@ -11750,14 +11805,6 @@ postcss-selector-parser@^6.0.2: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - postcss-svgo@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" @@ -11778,7 +11825,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.2.15: +postcss@^8.2.15, postcss@^8.2.9: version "8.4.31" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== @@ -11787,14 +11834,10 @@ postcss@^8.2.15: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.2.9: - version "8.4.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" - integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" +postinstall-postinstall@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" + integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== prelude-ls@^1.2.1: version "1.2.1" @@ -12823,6 +12866,13 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -13191,6 +13241,11 @@ skip-postinstall@^1.0.0: resolved "https://registry.yarnpkg.com/skip-postinstall/-/skip-postinstall-1.0.0.tgz#939d49b09ddae9816f089c0b892a8e4bb7bc7747" integrity sha512-IUVEmm4v7Ubzrp9JDG15oTzMB+abJdHcduXMRzBlHnHRrmpQ/QoPtYCRaorP+abAULTGEh87gPPyyMK5H1X1Dg== +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -15160,6 +15215,11 @@ yaml@^1.10.0, yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.2.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" + integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== + yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"